lib/ex_json_schema/validator/error/string_formatter.ex

defmodule ExJsonSchema.Validator.Error.StringFormatter do
  alias ExJsonSchema.Validator.Error

  @spec format(ExJsonSchema.Validator.errors()) :: [{String.t(), String.t()}]
  def format(errors) do
    Enum.map(errors, fn %Error{error: error, path: path} ->
      {to_string(error), path}
    end)
  end

  defimpl String.Chars, for: Error.AdditionalItems do
    def to_string(%Error.AdditionalItems{}) do
      "Schema does not allow additional items."
    end
  end

  defimpl String.Chars, for: Error.AdditionalProperties do
    def to_string(%Error.AdditionalProperties{}) do
      "Schema does not allow additional properties."
    end
  end

  defimpl String.Chars, for: Error.AllOf do
    def to_string(%Error.AllOf{invalid: invalid}) do
      "Expected all of the schemata to match, but the schemata at the following indexes did not: #{Enum.map_join(invalid, ", ", & &1.index)}."
    end
  end

  defimpl String.Chars, for: Error.AnyOf do
    def to_string(%Error.AnyOf{}) do
      "Expected any of the schemata to match but none did."
    end
  end

  defimpl String.Chars, for: Error.Const do
    def to_string(%Error.Const{expected: expected}) do
      "Expected data to be #{inspect(expected)}."
    end
  end

  defimpl String.Chars, for: Error.Contains do
    def to_string(%Error.Contains{}) do
      "Expected any of the items to match the schema but none did."
    end
  end

  defimpl String.Chars, for: Error.ContentEncoding do
    def to_string(%Error.ContentEncoding{expected: expected}) do
      "Expected the content to be #{expected}-encoded."
    end
  end

  defimpl String.Chars, for: Error.ContentMediaType do
    def to_string(%Error.ContentMediaType{expected: expected}) do
      "Expected the content to be of media type #{expected}."
    end
  end

  defimpl String.Chars, for: Error.Dependencies do
    def to_string(%Error.Dependencies{property: property, missing: [missing]}) do
      "Property #{property} depends on property #{missing} to be present but it was not."
    end

    def to_string(%Error.Dependencies{property: property, missing: missing}) do
      "Property #{property} depends on properties #{Enum.join(missing, ", ")} to be present but they were not."
    end
  end

  defimpl String.Chars, for: Error.Enum do
    def to_string(%Error.Enum{}) do
      "Value is not allowed in enum."
    end
  end

  defimpl String.Chars, for: Error.False do
    def to_string(%Error.False{}) do
      "False schema never matches."
    end
  end

  defimpl String.Chars, for: Error.Format do
    def to_string(%Error.Format{expected: expected}) do
      "Expected to be a valid #{format_name(expected)}."
    end

    defp format_name("date-time"), do: "ISO 8601 date-time"
    defp format_name("ipv4"), do: "IPv4 address"
    defp format_name("ipv6"), do: "IPv6 address"
    defp format_name(expected), do: expected
  end

  defimpl String.Chars, for: Error.IfThenElse do
    def to_string(%Error.IfThenElse{branch: branch}) do
      "Expected the schema in the #{branch} branch to match but it did not."
    end
  end

  defimpl String.Chars, for: Error.ItemsNotAllowed do
    def to_string(%Error.ItemsNotAllowed{}) do
      "Items are not allowed."
    end
  end

  defimpl String.Chars, for: Error.MaxItems do
    def to_string(%Error.MaxItems{expected: expected, actual: actual}) do
      "Expected a maximum of #{expected} items but got #{actual}."
    end
  end

  defimpl String.Chars, for: Error.MaxLength do
    def to_string(%Error.MaxLength{expected: expected, actual: actual}) do
      "Expected value to have a maximum length of #{expected} but was #{actual}."
    end
  end

  defimpl String.Chars, for: Error.MaxProperties do
    def to_string(%Error.MaxProperties{expected: expected, actual: actual}) do
      "Expected a maximum of #{expected} properties but got #{actual}"
    end
  end

  defimpl String.Chars, for: Error.Maximum do
    def to_string(%Error.Maximum{expected: expected, exclusive?: exclusive?}) do
      "Expected the value to be #{if exclusive?, do: "<", else: "<="} #{expected}"
    end
  end

  defimpl String.Chars, for: Error.MinItems do
    def to_string(%Error.MinItems{expected: expected, actual: actual}) do
      "Expected a minimum of #{expected} items but got #{actual}."
    end
  end

  defimpl String.Chars, for: Error.MinLength do
    def to_string(%Error.MinLength{expected: expected, actual: actual}) do
      "Expected value to have a minimum length of #{expected} but was #{actual}."
    end
  end

  defimpl String.Chars, for: Error.MinProperties do
    def to_string(%Error.MinProperties{expected: expected, actual: actual}) do
      "Expected a minimum of #{expected} properties but got #{actual}"
    end
  end

  defimpl String.Chars, for: Error.Minimum do
    def to_string(%Error.Minimum{expected: expected, exclusive?: exclusive?}) do
      "Expected the value to be #{if exclusive?, do: ">", else: ">="} #{expected}"
    end
  end

  defimpl String.Chars, for: Error.MultipleOf do
    def to_string(%Error.MultipleOf{expected: expected}) do
      "Expected value to be a multiple of #{expected}."
    end
  end

  defimpl String.Chars, for: Error.Not do
    def to_string(%Error.Not{}) do
      "Expected schema not to match but it did."
    end
  end

  defimpl String.Chars, for: Error.OneOf do
    def to_string(%Error.OneOf{valid_indices: valid_indices}) do
      if length(valid_indices) > 1 do
        "Expected exactly one of the schemata to match, but the schemata at the following indexes did: " <>
          Enum.join(valid_indices, ", ") <> "."
      else
        "Expected exactly one of the schemata to match, but none of them did."
      end
    end
  end

  defimpl String.Chars, for: Error.Pattern do
    def to_string(%Error.Pattern{expected: expected}) do
      "Does not match pattern #{inspect(expected)}."
    end
  end

  defimpl String.Chars, for: Error.PropertyNames do
    def to_string(%Error.PropertyNames{invalid: invalid}) do
      "Expected the property names to be valid but the following were not: #{invalid |> Map.keys() |> Enum.sort() |> Enum.join(", ")}."
    end
  end

  defimpl String.Chars, for: Error.Required do
    def to_string(%Error.Required{missing: [missing]}) do
      "Required property #{missing} was not present."
    end

    def to_string(%Error.Required{missing: missing}) do
      "Required properties #{Enum.join(missing, ", ")} were not present."
    end
  end

  defimpl String.Chars, for: Error.Type do
    def to_string(%Error.Type{expected: expected, actual: actual}) do
      "Type mismatch. Expected #{type_names(expected)} but got #{type_names(actual)}."
    end

    defp type_names(types) do
      types
      |> List.wrap()
      |> Enum.map(&String.capitalize/1)
      |> Enum.join(", ")
    end
  end

  defimpl String.Chars, for: Error.UniqueItems do
    def to_string(%Error.UniqueItems{}) do
      "Expected items to be unique but they were not."
    end
  end
end