Here’s another take that is explicit about the other optional fields
You can set the validation on the first of the optional fields or all of them depending on where you want to see error messages.
It provides a basic error message but should probably be used with an explicit error message.
def validate_one_of(changeset, field, other_fields, opts \\ []) do
fields = [field | List.wrap(other_fields)]
if Enum.all?(fields, fn field -> field_missing?(changeset, field) end) do
add_error(
changeset,
field,
opts[:message] || "either this or #{Enum.join(other_fields, ", ")} should be present"
)
else
changeset
end
end
with a few tests
describe "validate_one_of/2" do
test "invalid if none of the fields are present (first field gets error)" do
params = %{}
changeset =
Ecto.Changeset.cast({%{}, %{a: :string, b: :string, c: :string}}, params, [:a, :b, :c])
|> Helpers.validate_one_of(:a, [:b, :c],
message: "either this or b or c should be present"
)
refute changeset.valid?
assert [{:a, {"either this or b or c should be present", _}}] = changeset.errors
end
test "invalid if none of the fields are present (error on all fields)" do
params = %{}
changeset =
Ecto.Changeset.cast({%{}, %{a: :string, b: :string, c: :string}}, params, [:a, :b, :c])
|> Helpers.validate_one_of(:a, [:b, :c],
message: "either this or b or c should be present"
)
|> Helpers.validate_one_of(:b, [:a, :c],
message: "either this or a or c should be present"
)
|> Helpers.validate_one_of(:c, [:a, :b],
message: "either this or a or b should be present"
)
refute changeset.valid?
assert [
{:c, {"either this or a or b should be present", _}},
{:b, {"either this or a or c should be present", _}},
{:a, {"either this or b or c should be present", _}}
] = changeset.errors
end
test "valid if one of the fields is present" do
params = %{a: "a"}
changeset =
Ecto.Changeset.cast({%{}, %{a: :string, b: :string, c: :string}}, params, [:a, :b, :c])
|> Helpers.validate_one_of(:a, [:b, :c],
message: "either this or b or c should be present"
)
assert changeset.valid?
params = %{b: "b"}
changeset =
Ecto.Changeset.cast({%{}, %{a: :string, b: :string, c: :string}}, params, [:a, :b, :c])
|> Helpers.validate_one_of(:a, [:b, :c],
message: "either this or b or c should be present"
)
assert changeset.valid?
end
end