lib/fussy/validators/map.ex

defmodule Fussy.Validators.Map do
  @behaviour Fussy.Validator

  alias Fussy.Utils
  alias Fussy.ValidationError

  defstruct [:key, :value]

  @opaque t :: %__MODULE__{}

  @spec new(Fussy.Validator.t(), Fussy.Validator.t()) :: __MODULE__.t()
  def new(key, value) do
    %__MODULE__{key: key, value: value}
  end

  def validate(%__MODULE__{} = v, term), do: validate(v, [], term)

  @impl true
  def validate(%__MODULE__{}, _path, term) when is_map(term) and map_size(term) == 0,
    do: {:ok, %{}}

  @impl true
  def validate(%__MODULE__{key: key_v, value: value_v}, path, term) when is_map(term) do
    term
    |> Enum.reduce({%{}, []}, fn {key, value}, {map, errors} ->
      inner_path = path ++ [key]

      case {Utils.validate_using(key_v, inner_path, key),
            Utils.validate_using(value_v, inner_path, value)} do
        {{:ok, key}, {:ok, value}} ->
          {Map.put(map, key, value), errors}

        {{:error, key_errors}, _} ->
          {map, errors ++ key_errors}

        {_, {:error, value_errors}} ->
          {map, errors ++ value_errors}
      end
    end)
    |> then(fn
      {map, []} -> {:ok, map}
      {_, errors} -> {:error, errors}
    end)
  end

  @impl true
  def validate(%__MODULE__{}, path, term),
    do:
      {:error, [%ValidationError{mod: __MODULE__, path: path, msg: "must be a map", term: term}]}
end