Packages

A library for parsing, inspecting and manipulating JSON Schema documents.

Current section

Files

Jump to
json_schema lib parser util.ex
Raw

lib/parser/util.ex

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

  require Logger

  alias JsonSchema.{Parser, TypePath, Types}

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

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

  @doc ~S"""
  Creates a new type dictionary based on the given type definition
  and an optional ID.
  """
  @spec create_type_dict(
          Types.typeDefinition(),
          TypePath.t(),
          URI.t() | nil
        ) :: Types.typeDictionary()
  def create_type_dict(type_def, path, id) do
    string_path = path |> TypePath.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 ~S"""
  Returns a list of type paths when given a type dictionary.
  """
  @spec create_types_list(Types.typeDictionary(), TypePath.t()) :: [
          TypePath.t()
        ]
  def create_types_list(type_dict, path) do
    type_dict
    |> Enum.reduce(%{}, fn {child_abs_path, child_type}, reference_dict ->
      normalized_child_name = child_type.name
      child_type_path = TypePath.add_child(path, normalized_child_name)

      if child_type_path == TypePath.from_string(child_abs_path) do
        Map.merge(reference_dict, %{normalized_child_name => child_type_path})
      else
        reference_dict
      end
    end)
    |> Map.values()
  end

  @doc ~S"""
  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(), TypePath.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_name = to_string(idx)
      child_result = parse_type(child_node, parent_id, path, child_name)
      {ParserResult.merge(result, child_result), idx + 1}
    end)
    |> elem(0)
  end

  @spec parse_type(Types.schemaNode(), URI.t(), TypePath.t(), String.t()) ::
          ParserResult.t()
  def parse_type(schema_node, parent_id, path, name) 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 = TypePath.add_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 = TypePath.add_child(path, name)

          node_parser.(schema_node, child_parent_id, id, type_path, name)
      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
      ParserResult.merge(definitions_result, node_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

  # Determines the ID of a schema node based on its parent's ID and its own
  # optional '$id' or id' property.
  @spec determine_id(map, URI.t()) :: 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(%{"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
end