Search code examples
elixirphoenix-frameworkecto

Dynamically create JSON-api relationships in Elixir Phoenix


We have quite a few tests where we manually put in relationships. We do something like:

defp build_relationships(relationship_map, user) do
  relationship_map |> Map.put(:user, %{data: %{id: user.id}})
end

This works fine, but I want to make it flexible to accept any type of record (not just a user). For reference, user is created with ex-machina as insert(:user).

Is there a way to get the type of record that is passed in? If I could get a string that says "user", I could use String.to_atom(param) and pass that to Map.put but I can't find an elegant way to do this.

The real question is how do I take a record like user and return an atom of :user?

Any help is appreciated.


Solution

  • You can get some information about an Ecto struct using .__struct__.__schema__/1, for example:

    iex(1)> %Post{}.__struct__.__schema__(:source)
    "posts"
    

    but since there's no 1:1 mapping from a Schema to an ExMachina factory, there are 3 possible ways I can think of:

    1. Rename the factories to use the plural form, same as the name of the table, and use .__struct__.__schema__(:source) to get the name. This will make your factory code a bit weird to read as you'll then use insert(:users) to insert one user.

    2. Keep using singular names but use some library to convert plural table names to singular ones, e.g. something which converts "users" to "user" and "posts" to "post".

    3. Keep a list of Schema <-> Factory name mapping, like this (or you can even move the mapping to a separate function):

      defp build_relationships(relationship_map, struct) do
        mapping = %{MyApp.User => :user, MyApp.Post => :post} # add more here
        relationship_map |> Map.put(mapping[struct.__struct__], %{data: %{id: struct.id}})
      end
      
    4. Use Module.split/1 to get the name of the model and use Macro.underscore/1 to convert it to a lowercase + underscored name, and then use String.to_existing_atom:

      defp build_relationships(relationship_map, struct) do
        name = struct.__struct__ |> Module.split |> List.last |> Macro.underscore |> String.to_existing_atom
        relationship_map |> Map.put(name, %{data: %{id: struct.id}})
      end
      

      This will not work correctly if you use nested modules as the model, e.g. MyApp.Something.User.

    I would choose 4 if all your models are at the same level of nesting, or 3, if you want to be more explicit.