Search code examples
elixirecto

How can I add a mutual exclusion constraint with Ecto?


I've got a schema with two fields, a and b, and I want exactly one of the fields to be required. That is, if a is provided b should not be, and vice versa.

Is there a way to elegantly represent this with Ecto changeset validations? Something like this:

schema "foo" do
  field(:a, :string)
  field(:b, :string)
  field(:c, :string)

  timestamps()
end

def changeset(transaction, attrs) do
  transaction
  |> cast(attrs, [:a, :b, :c])
  |> validate_required([:c])
  |> validate_mutual_exclusion([:a, :b])
end

defp validate_mutual_exclusion(changeset, fields) do
  # What goes here?
end

Solution

  • You can count the number of fields that are present and check if that equals 1:

    defp validate_mutual_exclusion(changeset, fields) do
      present = Enum.count(fields, fn field -> present?(get_field(changeset, field)) end)
    
      case present do
        1 -> changeset # ok
        _ ->
          # add an error to each field
          Enum.reduce(fields, changeset, fn field, changeset ->
            add_error(changeset, field, "exactly one of these must be present: #{inspect(fields)}")
          end)
      end
    end
    

    present? just checks if the value is "" or nil:

    def present?(nil), do: false
    def present?(""), do: false
    def present?(_), do: true