defmodule OpenAPI.Spec.Schema do
@moduledoc "Raw JSON schema specification from the OpenAPI spec"
import OpenAPI.Reader.State
alias OpenAPI.Spec
alias OpenAPI.Spec.ExternalDocumentation
alias OpenAPI.Spec.Schema.Discriminator
alias OpenAPI.Spec.Schema.XML
#
# Definition
#
@typedoc """
JSON Schema specification
In addition to the data derived from the JSON or Yaml specification, this struct also contains
some contextual information including its absolute and referenced location in the spec. All
such fields are prefixed with `$oag_`.
"""
@type t :: %__MODULE__{
"$oag_base_file": String.t(),
"$oag_base_file_path": [Spec.path_segment()],
"$oag_last_ref_file": String.t() | nil,
"$oag_last_ref_path": [Spec.path_segment()],
"$oag_schema_context": [tuple],
title: String.t() | nil,
multiple_of: pos_integer | nil,
maximum: integer | nil,
exclusive_maximum: boolean,
minimum: integer | nil,
exclusive_minimum: boolean,
max_length: non_neg_integer | nil,
min_length: non_neg_integer | nil,
pattern: String.t() | nil,
max_items: non_neg_integer | nil,
min_items: non_neg_integer | nil,
unique_items: boolean,
max_properties: non_neg_integer | nil,
min_properties: non_neg_integer | nil,
required: [String.t()] | nil,
const: any,
enum: [any] | nil,
type: String.t() | nil,
all_of: [t | Spec.ref()] | nil,
one_of: [t | Spec.ref()] | nil,
any_of: [t | Spec.ref()] | nil,
not: t | Spec.ref() | nil,
items: t | Spec.ref() | nil,
properties: %{optional(String.t()) => t | Spec.ref()},
additional_properties: boolean | t | Spec.ref(),
description: String.t() | nil,
format: String.t() | nil,
default: any,
nullable: boolean,
discriminator: Spec.Schema.Discriminator.t(),
read_only: boolean,
write_only: boolean,
xml: Spec.Schema.XML.t(),
external_docs: Spec.ExternalDocumentation.t() | nil,
example: any,
deprecated: boolean
}
defstruct [
:"$oag_base_file",
:"$oag_base_file_path",
:"$oag_last_ref_file",
:"$oag_last_ref_path",
:"$oag_schema_context",
:title,
:multiple_of,
:maximum,
:exclusive_maximum,
:minimum,
:exclusive_minimum,
:max_length,
:min_length,
:pattern,
:max_items,
:min_items,
:unique_items,
:max_properties,
:min_properties,
:required,
:const,
:enum,
:type,
:all_of,
:one_of,
:any_of,
:not,
:items,
:properties,
:additional_properties,
:description,
:format,
:default,
:nullable,
:discriminator,
:read_only,
:write_only,
:xml,
:external_docs,
:example,
:deprecated
]
#
# Helpers
#
@doc false
@spec add_context(t, term) :: t
def add_context(%__MODULE__{"$oag_schema_context": contexts} = schema, new_context) do
%__MODULE__{schema | "$oag_schema_context": [new_context | contexts]}
end
@doc false
@spec merge_contexts(t, t) :: t
def merge_contexts(
%__MODULE__{"$oag_schema_context": contexts} = schema,
%__MODULE__{"$oag_schema_context": new_contexts}
) do
%__MODULE__{schema | "$oag_schema_context": new_contexts ++ contexts}
end
#
# Decoder
#
@doc false
@spec decode(map, map) :: {map, t}
def decode(state, yaml) do
{state, all_of} = decode_all_of(state, yaml)
{state, one_of} = decode_one_of(state, yaml)
{state, any_of} = decode_any_of(state, yaml)
{state, discriminator} = decode_discriminator(state, yaml)
{state, docs} = decode_external_docs(state, yaml)
{state, xml} = decode_xml(state, yaml)
{state, not_schema} = decode_not(state, yaml)
{state, items} = decode_items(state, yaml)
{state, properties} = decode_properties(state, yaml)
{state, additional_properties} = decode_additional_properties(state, yaml)
schema = %__MODULE__{
"$oag_base_file": Map.fetch!(state, :base_file),
"$oag_base_file_path": Map.fetch!(state, :base_file_path) |> Enum.reverse(),
"$oag_last_ref_file": Map.fetch!(state, :last_ref_file),
"$oag_last_ref_path": Map.fetch!(state, :last_ref_path) |> Enum.reverse(),
"$oag_schema_context": [],
title: Map.get(yaml, "title"),
multiple_of: Map.get(yaml, "multipleOf"),
maximum: Map.get(yaml, "maximum"),
exclusive_maximum: Map.get(yaml, "exclusiveMaximum", false),
minimum: Map.get(yaml, "minimum"),
exclusive_minimum: Map.get(yaml, "exclusiveMinimum", false),
max_length: Map.get(yaml, "maxLength"),
min_length: Map.get(yaml, "minLength"),
pattern: Map.get(yaml, "pattern"),
max_items: Map.get(yaml, "maxItems"),
min_items: Map.get(yaml, "minItems"),
unique_items: Map.get(yaml, "uniqueItems", false),
max_properties: Map.get(yaml, "maxProperties"),
min_properties: Map.get(yaml, "minProperties"),
required: Map.get(yaml, "required"),
const: Map.get(yaml, "const"),
enum: Map.get(yaml, "enum"),
type: Map.get(yaml, "type"),
all_of: all_of,
one_of: one_of,
any_of: any_of,
not: not_schema,
items: items,
properties: properties,
additional_properties: additional_properties,
description: Map.get(yaml, "description"),
format: Map.get(yaml, "format"),
default: Map.get(yaml, "default"),
nullable: Map.get(yaml, "nullable", false),
discriminator: discriminator,
read_only: Map.get(yaml, "readOnly", false),
write_only: Map.get(yaml, "writeOnly", false),
xml: xml,
external_docs: docs,
example: Map.get(yaml, "example"),
deprecated: Map.get(yaml, "deprecated", false)
}
{state, schema}
end
@spec decode_discriminator(map, map) :: {map, Discriminator.t() | nil}
defp decode_discriminator(state, %{"discriminator" => discriminator}),
do: with_path(state, discriminator, "discriminator", &Discriminator.decode/2)
defp decode_discriminator(state, _discriminator), do: {state, nil}
@spec decode_external_docs(map, map) :: {map, ExternalDocumentation.t() | nil}
defp decode_external_docs(state, %{"externalDocs" => docs}),
do: with_path(state, docs, "externalDocs", &ExternalDocumentation.decode/2)
defp decode_external_docs(state, _docs), do: {state, nil}
@spec decode_xml(map, map) :: {map, XML.t() | nil}
defp decode_xml(state, %{"xml" => xml}),
do: with_path(state, xml, "xml", &XML.decode/2)
defp decode_xml(state, _xml), do: {state, nil}
@spec decode_all_of(map, map) :: {map, t | nil}
defp decode_all_of(state, %{"allOf" => all_of}) do
with_path(state, all_of, "allOf", fn state, all_of ->
all_of
|> Enum.with_index()
|> Enum.reverse()
|> Enum.reduce({state, []}, fn {schema_or_ref, index}, {state, list} ->
{state, element} =
with_path(state, schema_or_ref, index, fn state, schema_or_ref ->
with_schema_ref(state, schema_or_ref, &decode/2)
end)
{state, [element | list]}
end)
end)
end
defp decode_all_of(state, _schema), do: {state, nil}
@spec decode_one_of(map, map) :: {map, t | nil}
defp decode_one_of(state, %{"oneOf" => one_of}) do
with_path(state, one_of, "oneOf", fn state, one_of ->
one_of
|> Enum.with_index()
|> Enum.reverse()
|> Enum.reduce({state, []}, fn {schema_or_ref, index}, {state, list} ->
{state, element} =
with_path(state, schema_or_ref, index, fn state, schema_or_ref ->
with_schema_ref(state, schema_or_ref, &decode/2)
end)
{state, [element | list]}
end)
end)
end
defp decode_one_of(state, _schema), do: {state, nil}
@spec decode_any_of(map, map) :: {map, t | nil}
defp decode_any_of(state, %{"anyOf" => any_of}) do
with_path(state, any_of, "anyOf", fn state, any_of ->
any_of
|> Enum.with_index()
|> Enum.reverse()
|> Enum.reduce({state, []}, fn {schema_or_ref, index}, {state, list} ->
{state, element} =
with_path(state, schema_or_ref, index, fn state, schema_or_ref ->
with_schema_ref(state, schema_or_ref, &decode/2)
end)
{state, [element | list]}
end)
end)
end
defp decode_any_of(state, _schema), do: {state, nil}
@spec decode_not(map, map) :: {map, t | nil}
defp decode_not(state, %{"not" => schema}) do
with_path(state, schema, "not", fn state, schema ->
with_schema_ref(state, schema, &decode/2)
end)
end
defp decode_not(state, _schema), do: {state, nil}
@spec decode_items(map, map) :: {map, t | nil}
defp decode_items(state, %{"items" => schema}) do
with_path(state, schema, "items", fn state, schema ->
with_schema_ref(state, schema, &decode/2)
end)
end
defp decode_items(state, _schema), do: {state, nil}
@spec decode_properties(map, map) :: {map, %{optional(String.t()) => t}}
defp decode_properties(state, %{"properties" => properties}) do
with_path(state, properties, "properties", fn state, properties ->
Enum.reduce(properties, {state, %{}}, fn
{key, schema}, {state, properties} ->
{state, property} =
with_path(state, schema, key, fn state, schema ->
with_schema_ref(state, schema, &decode/2)
end)
{state, Map.put(properties, key, property)}
end)
end)
end
defp decode_properties(state, _schema), do: {state, %{}}
@spec decode_additional_properties(map, map) :: {map, t | boolean}
defp decode_additional_properties(state, %{"additionalProperties" => value})
when is_boolean(value),
do: {state, value}
defp decode_additional_properties(state, %{"additionalProperties" => schema}) do
with_path(state, schema, "additionalProperties", fn state, schema ->
with_schema_ref(state, schema, &decode/2)
end)
end
defp decode_additional_properties(state, _schema), do: {state, true}
end