lib/fussy/validators/map.ex

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

  alias Fussy.Utils

  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

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

  @impl true
  def validate(%__MODULE__{key: key_validator, value: value_validator}, map) when is_map(map) do
    map
    |> Enum.reduce({%{}, []}, fn {key, value}, {valid_map, errors} ->
      case {Utils.validate_using(key_validator, key),
            Utils.validate_using(value_validator, value)} do
        {{:ok, key}, {:ok, value}} ->
          {Map.put(valid_map, key, value), errors}

        {{:error, reason}, _} ->
          {valid_map, ["key `#{inspect(key)}` => {#{reason |> Enum.join(", ")}}" | errors]}

        {_, {:error, reason}} ->
          {valid_map, ["#{inspect(key)} => {#{reason |> Enum.join(", ")}}" | errors]}
      end
    end)
    |> then(fn
      {valid_map, []} -> {:ok, valid_map}
      {_, errors} -> {:error, errors |> Enum.reverse()}
    end)
  end

  @impl true
  def validate(_, _), do: {:error, ["must be a map"]}
end