Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

elixir map with mixed keys

in the Phoenix application, I have a function that takes two maps, and creates two entries in the database via Ecto.Changeset.

def create_user_with_data(user_attrs, data_attrs) do
    name = cond do
        data_attrs["name"] ->
            data_attrs["name"]
        data_attrs[:name] ->
            data_attrs[:name]
        true -> nil
    end
    Ecto.Multi.new()
    |> Ecto.Multi.insert(:user, User.registration_changeset(%User{}, Map.put(user_attrs, :name, name)))
    |> Ecto.Multi.run(:user_data, fn(%{user: user}) ->
        %MyApp.Account.UserData{}
        |> MyApp.Account.UserData.changeset(Map.put(data_attrs, :user_id, user.id))
        |> Repo.insert()
    end)
    |> Repo.transaction()
end

because the keys in these map can be both atoms and lines, I have to check these keys.

but the expression

Map.put(user_attrs, :name, name)

will cause an error

** (Ecto.CastError) expected params to be a map with atoms or string keys, got a map with mixed keys: %{:name => "John", "email" => "[email protected]"}

if the keys are strings.

Is there any best practice in dealing with this issue?

like image 523
Marsel.V Avatar asked Nov 17 '17 16:11

Marsel.V


3 Answers

Explicitly cast all the keys to strings with Kernel.to_string/1:

data_attrs = for {k, v} <- data_attrs,
               do: {to_string(k), v}, into: %{}
like image 147
Aleksei Matiushkin Avatar answered Nov 09 '22 00:11

Aleksei Matiushkin


I'd convert all the keys to atoms first and then use atoms everywhere.

def key_to_atom(map) do
  Enum.reduce(map, %{}, fn
    {key, value}, acc when is_atom(key) -> Map.put(acc, key, value)
    # String.to_existing_atom saves us from overloading the VM by
    # creating too many atoms. It'll always succeed because all the fields
    # in the database already exist as atoms at runtime.
    {key, value}, acc when is_binary(key) -> Map.put(acc, String.to_existing_atom(key), value)
  end)
end

Then, convert pass all such maps through this function:

user_attrs = user_attrs |> key_to_atom
data_attrs = data_attrs |> key_to_atom

Now you can Map.put atom keys whenever you want to.

like image 4
Dogbert Avatar answered Nov 09 '22 00:11

Dogbert


I've used this recursive solution. Works great for nested map, and for mixed content (atom and string keys mixed) as well

defmodule SomeProject.MapHelper do
  def to_params(map) do
    Enum.map(map, &process_pair/1) |> Enum.into(%{})
  end

  defp process_pair({k, v}) do
    {"#{k}", process_value(v)}
  end

  defp process_value(v) when is_map(v) do
    to_params(v)
  end

  defp process_value(v), do: v
end

Here are the tests

defmodule CrecerInversiones.MapHelperTest do
  use CrecerInversiones.DataCase
  alias CrecerInversiones.MapHelper

  describe "to_params" do
    test "convert atom keys to strings" do
      params = MapHelper.to_params(%{a: "hi params"})

      assert params == %{"a" => "hi params"}
    end

    test "convert nested maps" do
      params = MapHelper.to_params(%{a: "hi params", b: %{z: "nested map"}})

      assert params == %{"a" => "hi params", "b" => %{"z" => "nested map"}}
    end

    test "accept mixed content" do
      params = MapHelper.to_params(%{"a" => "hi params", "b" => %{z: "nested map"}})

      assert params == %{"a" => "hi params", "b" => %{"z" => "nested map"}}
    end
  end
end
like image 1
Joe Esteves Avatar answered Nov 08 '22 22:11

Joe Esteves