Am I bastardizing Phoenix?

Hi All,

So I have just started a new project and I wanted to avoid some of the issues I had before, so I started going crazy with macros etc. Now I have something that half of me really likes, and the other half thinks Im doing things the wrong way. I’d like some feedback on whether my approaches are sensible or a complete discrace to the elixir community :).

Ok so the first issue I had was that I had occasionally ended up passing in the wrong value for functions like:

some_func(some_id, _some_other_id)

In this case I had a few small time wasting issues where I had these args in the wrong order. I had tests around this but sometimes with defdelgate in the mix etc I somehow still passed the wrong vars. In this case I really missed types, and I wished I had taken my input and cast it to a struct as early as possible.

Another issue I had is that I was taking in and returning snake cased json, which isn’t the convention in javascript.

Finally, my authorization was a mess of logic and I started to think about moving all this to one place such as described here: https://dockyard.com/blog/2017/08/01/authorization-for-phoenix-contexts

What I wanted to end up with is something that simplifies:

  1. Taking controller params input in and convert them a DTO struct
  2. Run validations against the input and bomb out if its invalid
  3. Use use this DTO to check user authorization
  4. If all well run the controller logic

In some ways I have achieved all of these but in other ways I might have gone a bit Javary to do so.

Here’s what my controller looks like now:

defmodule LunchFuWeb.AuthController do
  use LunchFuWeb.BaseController
  alias LunchFu.Dto
  alias Lunchfu.Accounts

  @bind request: Dto.LoginRequest
  def login(conn, _, request: login_request) do
    with {:ok, user} <- Accounts.login(login_request) do
      json(conn, user)
     end
  end

  @bind request: Dto.UpdateCredentialsRequest
  @can_handle :update_user, :request, LunchFu.DefaultUserGloves
  def update_user(conn, _, request: update_user_request) do
    with {:ok, user} <- Accounts.update_user(update_user_request) do
      json(conn, user)
     end
  end

end

The input mapping uses a small library to define how input should be mapped, and it is defined something like this:

defmodule CamelCaseKeyResolver do
  def resolve(key) do
    [h | t] =
      key
      |> Atom.to_string()
      |> Macro.camelize()
      |> String.split("", trim: true)

    String.downcase(h) <> Enum.join(t)
  end
end

defmodule LunchFu.Dto.LoginRequest do
  use ExMapper.DefMapping

  defstruct [:username, :password, :hashed_password]

  defmapping do
     keys &CamelCaseKeyResolver.resolve/1
     override :hashed_password, key: "password", value: &hash_password_input/1
  end

  def validate(request) do
    request
    |> Justify.validate_required(:username)
    |> Justify.validate_required(:password)
  end

  def hash_password_input(val) do
    # TODO: Hash password
    "SOMEHASHED_PASSWORD_EXAMPLE"
  end

end

The permissions are handled by a small dsl to define permissions that looks like this:

defmodule LunchFu.DefaultUserGloves do
  use Rubbergloves, wearer: LunchFu.User

  # Hardcoded rules checked first
  phase :pre_checks do
    can_handle!(%LunchFu.User{role: :admin}, _any_action) # Admin can do anything
    can_handle!(%LunchFu.User{name: "Christopher Owen"}, :update_user, request=%DTO.UpdateCredentialsRequest{}) # Hardcoded that I can update users
    cannot_handle!(_anyone, _any_action) # everyone else CANNOT do anything
  end
  
  # If rejected check database to see if I have explicit permissions
  phase :registery_check do
    can_handle?(user, action, conditions) do
      Repo.one(from r in PermissionsRegistory, where r.user_id == ^user.id and r.action == ^action and r.conditions == ^conditions) != nil
    end  
  end
  
end

So the good part to this is that I have a very clean controller, and the mapping/validation/auth logic is all separated. The bad parts are that I have a bunch of magic happening, and also I feel slightly disgusted in myself for using attributes a base controller as it feels very java like.

What are you thoughts on all of this?

Chris

2 Likes

Forgot to mention that also the reason I am responding with json(conn, schema) is because I also have other magic to map encode the schemas in a standard way always converting back to camelCase without using the phoinex views approach.

Looks alright to me. Phoenix is not strongly opinionated on how do you achieve your app’s goals. It only has a handful of conventions.

Everything that helps you maintain your code better is a win! If that’s the case then your code and concept are good. :slight_smile:

2 Likes

Thanks for the response, I was beginning to think it was the worst approach ever :). I’m still playing with all of this, but I’ll be pushing the code up to https://github.com/chrisjowen/rubbergloves if anyone else finds this even slightly useful.

1 Like