benco
benco

Reputation: 29

Elixir Ecto Changeset OR Validation

I am attempting to perform a Changeset validation on either an email OR phone number, and I found a nifty OR changeset function from @Dogbert here here - but I cannot get my OR validation flow to work correctly.

Does anyone mind taking a quick look on why the email or phone validation is always returning a nil changeset?

  @doc false
  def changeset(%User{} = user, attrs) do
    user
    |> cast(attrs, [:email, :phone])
    |> validate_required_inclusion([:email, :phone])
    |> validate_required_inclusion_format([:email, :phone])
  end

  defp validate_required_inclusion(changeset, fields) do
    if Enum.any?(fields, &present?(changeset, &1)) do
      changeset
    else
      # Add the error to the first field only since Ecto requires a field name for each error.
      add_error(changeset, hd(fields), "One of these fields must be present: #{inspect fields}")
    end
  end

  defp present?(changeset, field) do
    value = get_field(changeset, field)
    value && value != ""
  end

  ## TODO - this doesnt work
  defp validate_required_inclusion_format(changeset, fields) do
    if Enum.member?(fields, :email) do
      value = get_field(changeset, :email)
      if value && value != "" do
        IO.inspect(value, label: "email found: ")
        changeset
        |> email_changeset()
      end
    end
    if Enum.member?(fields, :phone) do
      value = get_field(changeset, :phone)
      if value && value != "" do
        IO.inspect(value, label: "phone found: ")
        changeset
        |> phone_changeset()
      end
    end
    changeset
  end

  defp email_changeset(changeset) do
    changeset
    |> validate_required([:email])
    |> validate_format(:email, ~r/.+@.+/)
    |> unique_constraint(:email)
  end

  defp phone_changeset(changeset) do
    changeset
    |> validate_required([:phone])
    |> valid_phone(:phone)
    |> unique_constraint(:phone)
  end

  defp valid_phone(changeset, field) do
    phone = get_field(changeset, field)
    IO.inspect(phone, label: "phone: ")
    {:ok, phone_number} = ExPhoneNumber.parse(phone, "US")
    IO.inspect(phone_number, label: "ExPhoneNumber: ")
    if ExPhoneNumber.is_valid_number?(phone_number) do
      changeset
    else
      changeset
      |> add_error(field, "Invalid Phone Number")
    end
  end

Thanks in advance!

Upvotes: 1

Views: 1115

Answers (1)

Dogbert
Dogbert

Reputation: 222080

You're not returning the modified changesets properly in validate_required_inclusion_format. In Elixir, the last value in a block is its return value. In if statements, the last value of both its true and false branch is its return value. If you don't have an else branch and the condition is false, the return value is nil.

Here's the simplest way to fix the problem: join the two top level if and the fallback changeset return with a ||:

defp validate_required_inclusion_format(changeset, fields) do
  if Enum.member?(fields, :email) do
    value = get_field(changeset, :email)
    if value && value != "" do
      IO.inspect(value, label: "email found: ")
      changeset
      |> email_changeset()
    end
  end || # <- note this
  if Enum.member?(fields, :phone) do
    value = get_field(changeset, :phone)
    if value && value != "" do
      IO.inspect(value, label: "phone found: ")
      changeset
      |> phone_changeset()
    end
  end || # <- and this
  changeset
end

Now if the first or second if conditions are not met, you'll get a nil and the third if will be evaluated. If the third or fourth also not met, the final fallback changeset will be returned.


Note: the naming of this function is misleading. Unlike the function you used from my previous answer, you're not using fields at all here. You're better off not passing fields to this function and calling it something like add_email_or_phone_changeset, e.g.

if value = get_field(changeset, :email) do
   ...
end ||
if value = get_field(changeset, :phone) do
   ...
end || ...

Upvotes: 2

Related Questions