Macro for controller

How can I make a macro for a controller, I want to login 2 different types of users UserClient, UserTransport that have many fields in common but are different, the login and the registration is basically the same in the controllers

defmodule PublitWeb.CliApiController do
  use PublitWeb, :controller
  alias Publit.{Repo, UserUtil, UserClient, SmsService}

# POST /cli_api/sessions
def create(conn, , %{"mobile_number" => mobile_number}) do
  case UserUtil.set_mobile_verification_token(UserClient, mobile_number) do
    {:ok, user} ->
      render(conn, "show.json", user: user, sms_gateway: SmsService.sms_gateway_num())
    _ ->
      conn
      |> put_status(:not_found)
      |> render("error.json", msg: gettext("Mobile number not found"))
  end
end

end

It would be nice to use the same macro like this

defmodule PublitWeb.CliApiController do
  use PublitWeb.AuthControllerMacro, mod: Publit.UserClient
end

and for the UserTransport

defmodule PublitWeb.TransApiController do
  use PublitWeb.AuthControllerMacro, mod: Publit.UserTransport
end

Couldn’t just have one general case function which implements the general case, and two specific functions which call it with fields set to their specific values?

4 Likes

I like the idea of moving the shared functionality into a separate module and then having the two controllers just call into that when needed, just to keep it simple, but I think behaviors might also work for your situation. If the two controllers share some actions but not others you could use behaviors like such:

defmodule ApiUserController do
  # These are the functions that are different for each controller and must be
  # implemented
  @callback register(conn, params)
  @callback login(conn, params)

  defmacro __using__(_) do
    quote do
      @behaviour ApiUserController
      # These are the functions that are shared across the different controller
      # types, though they can be overridden if needed
      def delete(conn, params) do ... end
      def view(conn, params) do ... end
      def whatever(conn, params) do ... end
      defoverridable ApiUserController
    end
  end
end

# cli registration and login implemented here
defmodule CliApiController do
  use PublitWeb, :controller
  use ApiUserController

  def register(conn, params) do ... end
  def login(conn, params) do ... end
end

# transit registration and login implemented here
defmodule TransApiController do
  use PublitWeb, :controller
  use ApiUserController

  def register(conn, params) do ... end
  def login(conn, params) do ... end
end

I don’t know if this is the route I would take though, especially if there are only two different user types. If the controllers are very similar you could use a single controller for both, but have the registration/login endpoints point to different actions: create_api_user and create_tranport_user. Then move whatever shared functionality into private functions inside the controller that each would call.

It feels like you are thinking about inheritance and the same old OO mantra still applies here: “prefer delegation over inheritance”.

For example, delegate the shared functions to a common controller

defmodule TransApiController do
  use PublitWeb, :controller

  def register(conn, params) do ... end
  def login(conn, params) do ... end

  def create(conn, params), do: Shared.create(Transit, conn, params)
  def update(conn, params), do: Shared.update(Transit, conn, params)
  def delete(conn, params), do: Shared.delete(Transit, conn, params)
end

or you inject the parameter in your Phoenix router:

resources "/transit", to: SharedControlller, assign: %{resource: Transit}
resources "/user", to: SharedControlller, assign: %{resource: User}

Whenever you see __using__ being used to reduce code duplication (i.e. it is not a callback in a behaviour) it is almost always a code smell.

5 Likes
def update(conn, params), to: Shared.update(Transit, conn, params)

Is this to: thing valid Elixir? I get a (CompileError) unexpected option :to in "try" on that line when trying on elixir 1.5.1.

probably just a typo for do: which would make it valid

1 Like

Those were all typos. Apologies, fixed now!

Really nice, I have tried to use a macro in the past but it was getting more complicated, but this way is really simple and clear thanks.