lib/parser/util.ex

defmodule JsonSchema.Parser.Util do
  @moduledoc """
  A module containing utility functions for JSON schema parsers.
  """

  require Logger

  alias JsonSchema.{Parser, Types}

  alias Parser.{
    AllOfParser,
    AnyOfParser,
    ArrayParser,
    DefinitionsParser,
    EnumParser,
    ErrorUtil,
    ObjectParser,
    OneOfParser,
    ParserResult,
    PrimitiveParser,
    TupleParser,
    TypeReferenceParser,
    UnionParser
  }

  @type nodeParser ::
          (Types.schemaNode(), URI.t(), URI.t(), URI.t(), String.t() ->
             ParserResult.t())

  @doc ~S"""
  Returns the name of the type of the given value.

  ## Examples

  iex> get_type([1,2,3])
  "list"

  iex> get_type(%{"type" => "string"})
  "object"

  iex> get_type("name")
  "string"

  iex> get_type(42)
  "integer"

  """
  @spec get_type(any) :: String.t()
  def get_type(value) when is_list(value), do: "list"
  def get_type(value) when is_map(value), do: "object"
  def get_type(value) when is_binary(value), do: "string"
  def get_type(value) when is_boolean(value), do: "boolean"
  def get_type(value) when is_float(value), do: "float"
  def get_type(value) when is_integer(value), do: "integer"
  def get_type(value) when is_nil(value), do: "null"
  def get_type(_value), do: "unknown"

  @doc """
  Creates a new type dictionary based on the given type definition
  and an optional ID.
  """
  @spec create_type_dict(
          Types.typeDefinition(),
          URI.t(),
          URI.t() | nil
        ) :: Types.typeDictionary()
  def create_type_dict(type_def, path, id) do
    string_path = path |> to_string()

    type_dict =
      if id != nil do
        string_id =
          if string_path == "#" do
            "#{id}#"
          else
            "#{id}"
          end

        %{string_path => type_def, string_id => type_def}
      else
        %{string_path => type_def}
      end

    type_dict
  end

  @doc """
  Returns a list of type paths when given a type dictionary.
  """
  @spec create_types_list(Types.typeDictionary(), URI.t()) :: [
          URI.t()
        ]
  def create_types_list(type_dict, path) do
    type_dict
    |> Enum.reduce(MapSet.new(), fn {child_abs_path, child_type}, acc_set ->
      normalized_child_name = child_type.name

      if normalized_child_name == :anonymous do
        child_abs_path_parts =
          child_abs_path
          |> String.split("/")

        child_prefix_path =
          child_abs_path_parts
          |> Enum.drop(-1)
          |> Enum.join("/")
          |> URI.parse()

        shares_prefix = child_prefix_path == path

        last_path_part_is_number =
          child_abs_path_parts
          |> List.last()
          |> Integer.parse() != :error

        if shares_prefix and last_path_part_is_number do
          MapSet.put(acc_set, URI.parse(child_abs_path))
        else
          acc_set
        end
      else
        child_type_path = add_fragment_child(path, normalized_child_name)

        if child_type_path == URI.parse(child_abs_path) do
          MapSet.put(acc_set, child_type_path)
        else
          acc_set
        end
      end
    end)
    |> MapSet.to_list()
  end

  @doc """
  Parse a list of JSON schema objects that have a child relation to another
  schema object with the specified `parent_id`.
  """
  @spec parse_child_types([Types.schemaNode()], URI.t(), URI.t()) ::
          ParserResult.t()
  def parse_child_types(child_nodes, parent_id, path)
      when is_list(child_nodes) do
    child_nodes
    |> Enum.reduce({ParserResult.new(), 0}, fn child_node, {result, idx} ->
      child_result =
        parse_type(
          child_node,
          parent_id,
          add_fragment_child(path, to_string(idx)),
          :anonymous
        )

      {ParserResult.merge(result, child_result), idx + 1}
    end)
    |> elem(0)
  end

  @doc """
  Parses a node type.
  """
  @spec parse_type(
          Types.schemaNode(),
          URI.t() | nil,
          URI.t(),
          String.t() | :anonymous,
          boolean
        ) ::
          ParserResult.t()
  def parse_type(schema_node, parent_id, path, name, name_is_regex \\ false) do
    definitions_result =
      if DefinitionsParser.type?(schema_node) do
        id = determine_id(schema_node, parent_id)
        child_parent_id = determine_parent_id(id, parent_id)
        type_path = add_fragment_child(path, name)

        DefinitionsParser.parse(
          schema_node,
          child_parent_id,
          id,
          type_path,
          name
        )
      else
        ParserResult.new(%{}, [], [])
      end

    node_result =
      case determine_node_parser(schema_node) do
        nil ->
          ParserResult.new(%{}, [], [])

        node_parser ->
          id = determine_id(schema_node, parent_id)
          child_parent_id = determine_parent_id(id, parent_id)
          type_path = add_fragment_child(path, name)

          node_parser.(schema_node, child_parent_id, id, type_path, name)
      end

    regex_result =
      if name_is_regex == true do
        case Regex.compile(name) do
          {:ok, _regex} ->
            ParserResult.new()

          {:error, _error} ->
            id = to_string(path)
            parser_error = ErrorUtil.name_not_a_regex(id, name)
            ParserResult.new(%{}, [], [parser_error])
        end
      else
        ParserResult.new()
      end

    if Enum.empty?(definitions_result.type_dict) and
         Enum.empty?(node_result.type_dict) do
      unknown_type_error = ErrorUtil.unknown_node_type(path, name, schema_node)
      ParserResult.new(%{}, [], [unknown_type_error])
    else
      definitions_result
      |> ParserResult.merge(node_result)
      |> ParserResult.merge(regex_result)
    end
  end

  @spec determine_node_parser(Types.schemaNode()) :: nodeParser | nil
  defp determine_node_parser(schema_node) do
    predicate_node_type_pairs = [
      {&AllOfParser.type?/1, &AllOfParser.parse/5},
      {&AnyOfParser.type?/1, &AnyOfParser.parse/5},
      {&ArrayParser.type?/1, &ArrayParser.parse/5},
      {&EnumParser.type?/1, &EnumParser.parse/5},
      {&ObjectParser.type?/1, &ObjectParser.parse/5},
      {&OneOfParser.type?/1, &OneOfParser.parse/5},
      {&PrimitiveParser.type?/1, &PrimitiveParser.parse/5},
      {&TupleParser.type?/1, &TupleParser.parse/5},
      {&TypeReferenceParser.type?/1, &TypeReferenceParser.parse/5},
      {&UnionParser.type?/1, &UnionParser.parse/5}
    ]

    {_pred?, node_parser} =
      predicate_node_type_pairs
      |> Enum.find({nil, nil}, fn {pred?, _node_parser} ->
        pred?.(schema_node)
      end)

    node_parser
  end

  @spec determine_id(map, URI.t() | nil) :: URI.t() | nil
  defp determine_id(%{"$id" => id}, parent_id) when is_binary(id) do
    do_determine_id(id, parent_id)
  end

  defp determine_id(_schema_node, _parent_id), do: nil

  @spec do_determine_id(String.t(), URI.t()) :: URI.t()
  defp do_determine_id(id, parent_id) do
    id_uri = URI.parse(id)

    if id_uri.scheme == "urn" do
      id_uri
    else
      URI.merge(parent_id, id_uri)
    end
  end

  @spec determine_parent_id(URI.t() | nil, URI.t()) :: URI.t()
  defp determine_parent_id(id, parent_id) do
    if id != nil and id.scheme != "urn" do
      id
    else
      parent_id
    end
  end

  @doc """
  Adds a child to the fragment of a `URI`.

  ## Examples

      iex> add_fragment_child(URI.parse("#/definitions/foo"), "bar")
      URI.parse("#/definitions/foo/bar")

  """
  @spec add_fragment_child(URI.t(), String.t() | :anonymous) :: URI.t()
  def add_fragment_child(uri, :anonymous) do
    uri
  end

  def add_fragment_child(uri, child) do
    old_fragment = uri.fragment

    new_fragment =
      case old_fragment do
        nil ->
          "/#{child}"

        "" ->
          "/#{child}"

        old_path ->
          Path.join(old_path, child)
      end

    %{uri | fragment: new_fragment}
  end
end