I don’t like having to write the same thing in multiple places. I know it’s not a big deal, but it annoys me.
Namely, the schema definition, the changeset cast, and the changeset validate_required. I would rather just write a map with all the data, and not worry about keeping everything synced
I’ve avoided writing macros until now, this might be an amusing learning experience.
What I want to do is something like this:
defmodule Player do
use Ecto.Schema
@required_fields_map %{email: :string}
@optional_fields_map %{foo: :string}
@required_fields_list Map.keys(@required_fields_map)
@optional_fields_list Map.keys(@optional_fields_map)
@all_fields_map Map.merge(@optional_fields_map, @required_fields_map)
@all_fields_list @required_fields_list ++ @optional_fields_list
# Some macro here to take @all_fields_map and transform it into:
#schema "player" do
# field :email :string
# field :foo :string
#end
changeset(player, params \\ %{}) do
player
|> Ecto.Changeset.cast(params, @all_fields_list)
|> Ecto.Changeset.validate_required(@required_fields_list)
end
end
Is it even possible to do this? Is there a better way?
I’m hardly a pro but this is what I do to reduce duplication.
Imagine having a user.ex file in a context:
@required [:email, :username]
def changeset_allowed(user, attrs, allowed) do
user
|> cast(attrs, allowed)
|> validate_required(@required -- @required -- allowed)
# TODO: add all of your validations for every field you plan to have, required or not.
end
And then create separate changesets with specific allowed rights, such as:
def admin_changeset(user, params \\ %{}) do
allowed = [
:admin?,
:email,
:username
]
user
|> changeset_allowed(params, allowed)
end
def email_address_changeset(user, params) do
allowed = [:email]
changeset_allowed(user, params, allowed)
end
def profile_changeset(user, params) do
allowed = [:username]
changeset_allowed(user, params, allowed)
end
The basic idea is even if your changeset_allowed has validations for 50 fields, only fields that are marked required / allowed will have their validations run against it when you call the individual specific changesets (which you would call for specific forms).
I feel it’s better to just be explicit here. Your schema and changeset fields won’t always match (for example, computed fields) and if you find yourself in that situation, you will now be faced with adding more to your DSL or now having two ways to define schema and changesets in the same app.
I come from Ruby and was a bit put off by this at first myself but have come to see not see it as a burden at all.