defmodule EctoCommons.PhoneNumberValidator do @moduledoc """ Validate a phone number. This validator uses libphonenumber under the hood to validate phone numbers. This validator expects a `:country` argument to be passed so it can check the number corresponds to the given country. If you pass it `""` or `nil` it won't complain as long as the number is a valid E164 format that can be parsed without the country info. ## Examples iex> types = %{phone_number: :string} iex> params = %{phone_number: "0610234567"} iex> Ecto.Changeset.cast({%{}, types}, params, Map.keys(types)) ...> |> validate_phone_number(:phone_number, country: "fr") #Ecto.Changeset iex> types = %{phone_number: :string} iex> params = %{phone_number: "798765432L"} iex> Ecto.Changeset.cast({%{}, types}, params, Map.keys(types)) ...> |> validate_phone_number(:phone_number, country: "ch") #Ecto.Changeset # Country can be ignored in E164 formatted numbers iex> types = %{phone_number: :string} iex> params = %{phone_number: "+16502530000"} iex> Ecto.Changeset.cast({%{}, types}, params, Map.keys(types)) ...> |> validate_phone_number(:phone_number) #Ecto.Changeset iex> types = %{phone_number: :string} iex> params = %{phone_number: "01 23 45 67 89"} iex> Ecto.Changeset.cast({%{}, types}, params, Map.keys(types)) ...> |> validate_phone_number(:phone_number, country: nil) #Ecto.Changeset """ import Ecto.Changeset def validate_phone_number(changeset, field, opts \\ []) do validate_change(changeset, field, {:phone_number, opts}, fn _, value -> country_isoalpha2 = Keyword.get(opts, :country) || "" country_isoalpha2 = String.upcase(country_isoalpha2) with {:ok, parsed} <- ExPhoneNumber.parse(value, country_isoalpha2), true <- ExPhoneNumber.is_valid_number?(parsed) do [] else _err -> [{field, {message(opts, "is not a valid phone number"), [validation: :phone_number]}}] end end) end defp message(opts, key \\ :message, default) do Keyword.get(opts, key, default) end end