lib/ex_json_schema/validator/properties.ex

defmodule ExJsonSchema.Validator.Properties do
  @moduledoc """
  `ExJsonSchema.Validator` implementation for `"properties"` attributes.

  See:

  """

  alias ExJsonSchema.Validator
  alias ExJsonSchema.Validator.Error

  @behaviour ExJsonSchema.Validator

  @impl ExJsonSchema.Validator
  def validate(root, schema, {"properties", _}, properties = %{}, path) do
    do_validate(root, schema, properties, path)
  end

  def validate(_, _, _, _, _) do
    []
  end

  defp do_validate(root, schema, properties, path) do
    validated_known_properties = validate_known_properties(root, schema, properties, path)

    validation_errors(validated_known_properties) ++
      validate_additional_properties(
        root,
        schema["additionalProperties"],
        unvalidated_properties(properties, validated_known_properties),
        path
      )
  end

  defp validate_known_properties(root, schema, properties, path) do
    validate_named_properties(root, schema["properties"], properties, path) ++
      validate_pattern_properties(root, schema["patternProperties"], properties, path)
  end

  defp validate_named_properties(root, schema, properties, path) do
    schema
    |> Enum.filter(&Map.has_key?(properties, elem(&1, 0)))
    |> Enum.map(fn
      {name, property_schema} ->
        {name, Validator.validation_errors(root, property_schema, properties[name], path <> "/#{name}")}
    end)
  end

  defp validate_pattern_properties(_, nil, _, _), do: []

  defp validate_pattern_properties(root, schema, properties, path) do
    Enum.flat_map(schema, &validate_pattern_property(root, &1, properties, path))
  end

  defp validate_pattern_property(root, {pattern, schema}, properties, path) do
    properties_matching(properties, pattern)
    |> Enum.map(fn {name, property} ->
      {name, Validator.validation_errors(root, schema, property, path <> "/#{name}")}
    end)
  end

  defp validate_additional_properties(root, schema, properties, path) when is_map(schema) do
    Enum.flat_map(properties, fn {name, property} ->
      Validator.validation_errors(root, schema, property, path <> "/#{name}")
    end)
  end

  defp validate_additional_properties(_, false, properties, path) when map_size(properties) > 0 do
    Enum.map(properties, fn {name, _} ->
      %Error{error: %Error.AdditionalProperties{}, path: path <> "/#{name}"}
    end)
  end

  defp validate_additional_properties(_, _, _, _), do: []

  defp validation_errors(validated_properties) do
    validated_properties |> Keyword.values() |> List.flatten()
  end

  defp properties_matching(properties, pattern) do
    regex = Regex.compile!(pattern)
    Enum.filter(properties, &Regex.match?(regex, elem(&1, 0)))
  end

  defp unvalidated_properties(properties, validated_properties) do
    keys =
      properties
      |> keys_as_set()
      |> MapSet.difference(keys_as_set(validated_properties))
      |> Enum.to_list()

    Map.take(properties, keys)
  end

  defp keys_as_set(properties) do
    properties |> Enum.map(&elem(&1, 0)) |> Enum.into(MapSet.new())
  end
end