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?
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: %{}
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With