Create new guard based on Map

Hi,

I’m working on an API and I need to maintain previous versions. My current design is:

  • a Plug which extract the version from Headers, validates it, and stores it in conn.assigns
  • a pattern matching in my controllers to branch on different versions when needed

For example, in my controllers, I have things like:

defmodule MyApp.MyController do
  use MyApp.Web, :controller

  def create(conn=%Plug.Conn{assigns: %{api_version: 20160422}}, %{"data" => %{"attributes" => params}}) do
    # ...
  end
end

It works well but typing the whole %Plug.Conn{assigns: %{api_version: 20160422}} every time I need to specify a version is very tedious, and clutter a bit the function definition. Moreover, if I decide to change my assign variable, I would have to update a bunch of functions.

I thought it would be great to create a guard for this, an example of what I wanted:

defmodule MyApp.MyController do
  use MyApp.Web, :controller

  def create(conn, %{"data" => %{"attributes" => params}}) when api_version(conn, 20160422) do
    # ...
  end
end

Unfortunately the list of functions available available to create guards (https://github.com/itsgreggreg/elixir_quick_reference#guards) doesn’t allow me to work with Map.

Do you have any suggestion about how I could make it work?

If guards are not possible, another solution which came in my mind, is to defined a default new, create,… to process the version and call the same method with one more parameter. For example:

# my_app/web.ex
# ... in def controller do
def create(conn=%Plug.Conn{assigns: %{api_version: version}}, params) do
  create(conn, version, params)
end

# my_app/my_controller.ex
defmodule MyApp.MyController do
  use MyApp.Web, :controller

  def create(conn, 20160422, %{"data" => %{"attributes" => params}}) do
    # ...
  end

  # or, if I don't care about the version
  def create(conn, _version, %{"data" => %{"attributes" => params}}) do
    # ...
  end
end

Thanks for you suggestions/feedbacks :slight_smile:

1 Like

Have you considered using different scopes for different versions of your API in the web/router.ex? Of course this would also mean to have different URIs for the different API-versions, but somewhere you have to put the clients version anyway.

1 Like

Yes, but the the API architecture use 2 different schemas and so we really don’t want to change the URL each time we deploy a breaking change.

Basically, we have /v1 in the URL, but we will probably not change that before a long time. I guess until we break everything or rewrite it.

The current API still evolves over time, some breaking changes happen (slightly different input/output/new endpoints/endpoints deletion). That’s what the version in headers is used for. The API user can select the version he wants by setting the correct header (containing the release date).

So basically we use 2 different versioning:

  • The major, in the URL, which is not going to change before a long time
  • The minor, in headers, which changes each time we deploy a breaking change

As an example you can also look at https://stripe.com/docs/api#versioning. They have /v1/ in the URL but still, they handle another versioning which allow the API user to set the exact version he wants.

I hope that makes sense.

1 Like