How do I define custom constraints as part of an embedded schema's changeset function?

Due to the nature of my work, I often work with attribute-value matrices (AVMs, or just maps, in the world of Elixir) on which complex constraints are defined. This kind of work I normally do outside of Elixir, since there are dedicated toolchains for that already. However, I was wondering: What if I’d like to do the same work in Elixir? Would it be possible?

So, I started exploring typed AVMs first, which can be modelled quite well as maps validated with the help of an Ecto embedded schema. During validation via changeset, however, it appears to not be possible to define custom constraints, for example dependency constraints such as the value of some field having to be the same as that of another field. Am I getting this right?

If yes, are there otherwise libraries that deal with kind of problems?

If not, is it a desideratum? I could work on that myself.

Even if there are lot of validations you are looking for missing, making them yourself is fairly trivial. For example look at the source code for validate_length:

 @spec validate_length(t, atom, Keyword.t()) :: t
  def validate_length(changeset, field, opts) when is_list(opts) do
    validate_change(changeset, field, {:length, opts}, fn
      _, value ->
        count_type = opts[:count] || :graphemes

        {type, length} =
          case {value, count_type} do
            {value, :codepoints} when is_binary(value) ->
              {:string, codepoints_length(value, 0)}

            {value, :graphemes} when is_binary(value) ->
              {:string, String.length(value)}

            {value, :bytes} when is_binary(value) ->
              {:binary, byte_size(value)}

            {value, _} when is_list(value) ->
              {:list, list_length(changeset, field, value)}

            {value, _} when is_map(value) ->
              {:map, map_size(value)}
          end

        error =
          ((is = opts[:is]) && wrong_length(type, length, is, opts)) ||
            ((min = opts[:min]) && too_short(type, length, min, opts)) ||
            ((max = opts[:max]) && too_long(type, length, max, opts))

        if error, do: [{field, error}], else: []
    end)
  end

And this is a more involved one because of all the options provided, usually when I make custom validations, they are very short.

Also take a look at nimble_options, even though that library is very good, ecto beats it in terms of custom validations.

2 Likes

There’s also things like solnic’s drops library
https://hexdocs.pm/drops/readme.html

solnic is the creator/maintainer of the dry-rb ecosystem of libraries so has a ton of experience with validation patterns

2 Likes