lib/schema_validator.ex

defmodule Mecto.SchemaValidator do
  @moduledoc """
  Validates fields extracted with `Mecto.MarkupParser` exist based on the schema given from
  `Mecto.MarkupParser`.
  """

  alias Mecto.SchemaValidator.Result

  def check(schema, nodes) do
    result = check(schema, nodes, [])

    if result.error == [] do
      result.ok
    else
      {:error, result.error}
    end
  end

  defp check(schema, nodes, path),
    do: Enum.reduce(nodes, %Result{path: path}, &check_node(schema, &1, &2))

  defp check_node(schema, {node, nested_nodes}, %Result{path: path} = result) do
    case Map.get(schema, node) do
      nil ->
        Result.add_error(result, node, "does not exist")

      value when is_atom(value) ->
        Result.merge(result, node, value)

      enum when is_map(enum) ->
        nested_result = check(enum, nested_nodes, path ++ [node])
        Result.merge(result, node, nested_result)

      {cardinality, enum} when is_map(enum) ->
        integer_keys? =
          nested_nodes
          |> Map.keys()
          |> Enum.all?(&is_integer/1)

        case {cardinality, integer_keys?} do
          {:one, false} ->
            nested_result = check(enum, nested_nodes, path ++ [node])
            Result.merge(result, node, nested_result)

          {:many, true} ->
            nested_result =
              Enum.map(nested_nodes, fn {index, nested_nodes} ->
                {index, check(enum, nested_nodes, path ++ [node, index])}
              end)

            Result.merge(result, node, nested_result)

          {:one, true} ->
            Result.add_error(
              result,
              node,
              "has a cardinality of :one, but is being used like a list"
            )

          {:many, false} ->
            Result.add_error(
              result,
              node,
              "have a cardinality of :many, but is being used like a single element"
            )
        end
    end
  end
end