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, examples: list() | nil, 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, :examples, :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"), examples: Map.get(yaml, "examples"), 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