Pattern Matching to transform Map shape?

I had a problem that came up from a result of trying to obscure genuinely weird database column names in a legacy (shared) database. I got Ecto pointed to the database, and I can modify my views to make it return JSON responses that use sensible names for fields. HOWEVER, I’m getting into trouble when I’m trying to support POST and PATCH requests. My thought was that I could allow the JSON format to be sane, and then in my controller I could translate from normal field name to the one actually used by Ecto and the database… but I’ve found myself lost in pattern-matching confusion. Can someone explain what I’m doing wrong?

For example, I make a PATCH request:

{
    "something": {
        "status": 3
    }
}

But really, my field name is something redundant like “something_status” (just as an example). How can I translate between the 2 formats before I pass the map object on to the update operation?

It would be easier to answer this question if you supplied some code.

2 Likes

Sorry, got distracted before I posted the code. Here’s what my controller has currently (this causes errors):

  def update(conn, %{"id" => id, "collection" => collection_params}) do
    IO.inspect(collection_params)

    %{
      "status" => status,
      "description" => description
    } = collection_params

    translated_params = %{
      collection_status: status,
      collection_description: description
    }

    IO.inspect(translated_params)

    collection = Resource.get_collection!(id)

    with {:ok, %Collection{} = collection} <- Resource.update_collection(collection, translated_params) do
      render(conn, "show.json", collection: collection)
    end
  end

I want to allow the clients to POST/PATCH data that uses a normal key such as “status”… but the changeset needs to act on the actual database column name (collection_status).

Ok, I figured out a way to do this – it boils down to this: create a new map of parameters whose keys have been translated from the clean version that would get posted/patched into the legacy key names as used by the legacy database and Ecto. Something like this:

translated_params = Map.new()

if (raw_params["foo"]) do
  translated_params = Map.put(translated_params, "legacy_foo_used_by_ecto", raw_params["foo"])
end

# ... etc... translate each parameter if available

Although this appears to work, it seems smelly and not idiomatic Elixir code. If anyone has any ideas for how to improve on this, I’m all ears. Thanks!

You could just make a copy of the original table, translated_params = raw_params then Enum.filter to remove the ones you don’t want.

iex(2)> key_translations = %{"foo" => "legacy_foo_used_by_ecto"}
iex(3)> raw_params = %{"foo" => 1, "bar" => 2}
iex(4)> translated_params = Map.new(raw_params, fn {k,v} -> {Map.get(key_translations, k, k), v} end)
%{"bar" => 2, "legacy_foo_used_by_ecto" => 1}

Basic idea is instead of encoding in the conditional structure of your program with if statements, instead create a map that has the desired key translations. Then use Map.new/2 to apply a transformation (defaulting to the existing key if it is not in the key_translations).

Thank you! I’m very flattered to have your response. I’ll try this a few different ways as I get a better feel for the language.

Thank you – yes, that’s a much better approach.

I also just noticed that Ecto supports a @field_source_mapper which seems to support this exact use case (?), but I can’t seem to find an example…

Right. I just gave you a very tactical way of mapping one map’s keys, but you are absolutely correct in the bigger picture. You should let Ecto handle this for you. Rather than dealing with @field_source_mapper I’d suggest you look at the :source option of each field. The mapper would be useful if there were some consistent pattern (such as the db prefixes every field with “col_” or something) but it’s just an abstraction over this :source option.

It’s all good for learning, thanks! It seems to have worked exactly as I had hoped by updating the schema in the model to include the source option, e.g.

field :foo, :string, [source: :legacy_foo_with_weird_name]

but I’m glad I have learned some other bits of syntax…

1 Like