defmodule JsonSchema.Parser.ObjectParser do
@behaviour JsonSchema.Parser.ParserBehaviour
@moduledoc """
Parses a JSON schema object type:
{
"type": "object",
"properties": {
"color": {
"$ref": "#/color"
},
"title": {
"type": "string"
},
"radius": {
"type": "number"
}
},
"required": [ "color", "radius" ]
}
Into an `JsonSchema.Types.ObjectType`
"""
require Logger
alias JsonSchema.{Parser, Types}
alias Parser.{ParserResult, Util}
alias Types.ObjectType
@doc """
Returns true if the json subschema represents an allOf type.
## Examples
iex> type?(%{})
false
iex> an_object = %{"properties" => %{"name" => %{"type" => "string"}}}
iex> type?(an_object)
true
"""
@impl JsonSchema.Parser.ParserBehaviour
@spec type?(Types.schemaNode()) :: boolean
def type?(%{"properties" => properties}) when is_map(properties), do: true
def type?(_schema_node), do: false
@doc """
Parses a JSON schema object type into an `JsonSchema.Types.ObjectType`.
"""
@impl JsonSchema.Parser.ParserBehaviour
@spec parse(Types.schemaNode(), URI.t() | nil, URI.t(), URI.t(), String.t()) ::
ParserResult.t()
def parse(schema_node, parent_id, id, path, name) do
required = Map.get(schema_node, "required", [])
description = Map.get(schema_node, "description")
default = Map.get(schema_node, "default")
properties_path = Util.add_fragment_child(path, "properties")
properties_result =
schema_node
|> Map.get("properties")
|> parse_child_types(parent_id, properties_path)
properties_type_dict = create_property_dict(properties_result.type_dict, properties_path, id)
pattern_properties_path = Util.add_fragment_child(path, "patternProperties")
pattern_properties = schema_node["patternProperties"]
pattern_properties_result =
if is_map(pattern_properties) do
pattern_properties
|> parse_child_types(parent_id, pattern_properties_path, true)
else
ParserResult.new()
end
pattern_properties_type_dict =
create_property_dict(
pattern_properties_result.type_dict,
pattern_properties_path,
id
)
additional_properties = schema_node["additionalProperties"]
{additional_properties_value, additional_properties_result} =
cond do
is_boolean(additional_properties) ->
{additional_properties, ParserResult.new()}
is_map(additional_properties) ->
parser_result =
schema_node
|> Map.get("additionalProperties")
|> Util.parse_type(parent_id, path, "additionalProperties")
if parser_result != nil do
{Util.add_fragment_child(path, "additionalProperties"), parser_result}
else
{nil, ParserResult.new()}
end
true ->
{nil, ParserResult.new()}
end
object_type = %ObjectType{
name: name,
description: description,
default: default,
path: path,
properties: properties_type_dict,
pattern_properties: pattern_properties_type_dict,
additional_properties: additional_properties_value,
required: required
}
object_type
|> Util.create_type_dict(path, id)
|> ParserResult.new()
|> ParserResult.merge(properties_result)
|> ParserResult.merge(pattern_properties_result)
|> ParserResult.merge(additional_properties_result)
end
@spec parse_child_types(map, URI.t() | nil, URI.t(), boolean) :: ParserResult.t()
defp parse_child_types(
node_properties,
parent_id,
child_path,
name_is_regex \\ false
) do
init_result = ParserResult.new()
node_properties
|> Enum.reduce(init_result, fn {child_name, child_node}, acc_result ->
child_types =
Util.parse_type(
child_node,
parent_id,
child_path,
child_name,
name_is_regex
)
ParserResult.merge(acc_result, child_types)
end)
end
@doc """
Creates a property dictionary based on a type dictionary and a type path.
## Examples
iex> type_dict = %{}
...> path = URI.parse("#")
...> id = "http://www.example.com/root.json"
...> JsonSchema.Parser.ObjectParser.create_property_dict(type_dict, path, id)
%{}
"""
@spec create_property_dict(Types.typeDictionary(), URI.t(), URI.t() | nil) ::
Types.propertyDictionary()
def create_property_dict(type_dict, path, id) do
type_dict
|> Enum.reduce(%{}, fn {child_path, child_type}, acc_property_dict ->
if is_immediate_child(child_path, child_type.name, path, id) do
child_type_path = Util.add_fragment_child(path, child_type.name)
child_property_dict = %{child_type.name => child_type_path}
Map.merge(acc_property_dict, child_property_dict)
else
acc_property_dict
end
end)
end
@spec is_immediate_child(URI.t(), String.t(), URI.t(), URI.t() | nil) ::
boolean
defp is_immediate_child(child_path, child_name, properties_path, id) do
child_path_alt = Util.add_fragment_child(properties_path, child_name)
if id == nil do
to_string(child_path) == to_string(child_path_alt)
else
absolute_child_path_alt = %{id | fragment: child_path_alt.fragment}
to_string(child_path) == to_string(child_path_alt) ||
to_string(child_path) == to_string(absolute_child_path_alt)
end
end
end