defmodule PasswordValidator do @moduledoc """ Primary interface to PasswordValidator. The two main methods are `validate/3` and `validate_password/2`. ## Examples iex> opts = [ ...> length: [max: 6], ...> ] iex> PasswordValidator.validate_password("too_long", opts) {:error, ["String is too long. 8 but maximum is 6"]} iex> opts = [ ...> length: [min: 5, max: 30], ...> character_set: [ ...> lower_case: 1, # at least one lower case letter ...> upper_case: [3, :infinity], # at least three upper case letters ...> numbers: [0, 4], # at most 4 numbers ...> special: [0, 0], # no special characters allowed ...> ] ...> ] iex> changeset = Ecto.Changeset.change({%{password: "Simple_pass12345"}, %{}}, %{}) iex> changeset = PasswordValidator.validate(changeset, :password, opts) iex> changeset.errors [password: {"Too many special (1 but maximum is 0)", [ {:validator, PasswordValidator.Validators.CharacterSetValidator}, {:error_type, :too_many_special} ]}, password: {"Too many numbers (5 but maximum is 4)", [ {:validator, PasswordValidator.Validators.CharacterSetValidator}, {:error_type, :too_many_numbers} ]}, password: {"Not enough upper_case characters (only 1 instead of at least 3)", [ {:validator, PasswordValidator.Validators.CharacterSetValidator}, {:error_type, :too_few_upper_case} ]}] """ alias PasswordValidator.Validators @validators [ Validators.LengthValidator, Validators.CharacterSetValidator ] @spec validate(Ecto.Changeset.t(), atom(), list()) :: Ecto.Changeset.t() def validate(changeset, field, opts \\ []) do password = Ecto.Changeset.get_field(changeset, field) case do_validate(password, opts) do :ok -> changeset {:error, errors} -> Enum.reduce(errors, changeset, fn error, cset when is_binary(error) -> Ecto.Changeset.add_error(cset, field, error) {error_message, additional_info}, cset when is_binary(error_message) -> Ecto.Changeset.add_error(cset, field, error_message, additional_info) end) end end defp do_validate(password, opts) do results = validators(opts) |> Enum.map(&run_validator(&1, password, opts)) errors = for({:error, reason} <- results, do: reason) |> List.flatten() if length(errors) > 0 do {:error, errors} else :ok end end @spec validate_password(String.t(), list()) :: :ok | {:error, nonempty_list()} def validate_password(password, opts \\ []) do results = validators(opts) |> Enum.map(&run_validator(&1, password, opts)) errors = for {:error, errors} <- results do Enum.map(errors, fn {error_message, _} when is_binary(error_message) -> error_message error_message when is_binary(error_message) -> error_message end) end |> List.flatten() if length(errors) > 0 do {:error, errors} else :ok end end defp run_validator(validator, password, opts) do validator.validate(password, opts) end defp validators(opts) do additional_validators(opts) |> Enum.concat(@validators) end defp additional_validators(opts) do case Keyword.get(opts, :additional_validators, []) do validators when is_list(validators) -> validators non_list -> raise "Expected a list of validators, instead received #{inspect(non_list)}" end end end