Hi, I am confused about different behavior in changesets when input data has atom
vs string
keys.
The Scenario
For example, let’s say I have a Company schema and changeset:
schema "company" do
email: :string
many_to_many: :categories, join_through: "companies_categories"
has_many :addresses, Addresses.Address
end
def changeset(attrs) do
%Company{}
|> cast([:email])
|> cast_assoc(:address)
end
I am omitting the definition of Address
schema for the sake of brevity.
Then, considering I have the following data arriving at the controller:
%{
"company" => %{
"email" => "some@email.com",
"categories_ids" => [1, 2],
"address" => %{
"address_line1" => "some address_line1"
"city_id" => 2
}
}
}
The controller will receive it and call a function defined in my context module that will do the hard work:
# in CompanyController:
def create_company(conn, %{"company" => attrs}) do
Accounts.create_company(attrs)
# ...
end
# in Accounts module
def create_company(attrs) do
company_changeset = Company.changeset(attrs)
categories_ids_changeset = categories_ids_changeset(attrs)
# ... create the company,
# obtain the categories_ids from its changeset,
# retrieve the categories from db using the ids,
# and put_assoc(:categories, company)
end
def categories_ids_changesest(attrs) do
types = %{categories_ids: {:array, :integer}}
{attrs, types}
|> cast(attrs, Map.keys(types))
end
In this case I am using a schemaless changeset to validate input data that is slightly different from data in my schema
.
The struggle:
I can not find a standard way to retrieve the changes from categories_ids_changeset
.
So, ok, I can do categories_ids_changeset.changes
. That will work when the input data has string
keys. But it will not if it has atom
keys. See:
iex(80)> attrs = %{"categories_ids" => [1, 2]}
iex(81)> types = %{categories_ids: {:array, :integer}}
iex(82)> changeset = {attrs, types} |> Changeset.cast(attrs, Map.keys(types))
#Ecto.Changeset<action: nil, changes: %{categories_ids: [1, 2]}, errors: [],
data: %{"categories_ids" => [1, 2]}, valid?: true>
iex(84)> attrs = %{categories_ids: [1, 2]}
iex(85)> types = %{categories_ids: {:array, :integer}}
iex(86)> changeset = {attrs, types} |> Changeset.cast(attrs, Map.keys(types))
#Ecto.Changeset<action: nil, changes: %{}, errors: [],
data: %{categories_ids: [1, 2]}, valid?: true>
Note that in the second changeset, whose initial attrs
had atom
keys, changes
is empty!
I tried to appy_changes/1
in the changeset, but when there are string keys, it will generate a map with mixed keys…
%{:categories_ids => [1, 2], "categories_ids" => [1, 2]}
This behavior is messing up with my tests, as for testing CompanyController.create_company/2
, I pass data with string keys, but when testing my context modules, i.e. Accounts.create_company
, I tend to use atom keys.
I thought about using apply_changes/1
and they filter out string
keys… but I don’t know, it seems hacky, so maybe I am missing something simpler that can solve it in a better way.
Thanks in advance.