lib/ex_oapi/parser/context/schema.ex

defmodule ExOAPI.Parser.V3.Context.Schema do
  use TypedEctoSchema

  import Ecto.Changeset
  import ExOAPI.Helpers.Casting, only: [translate: 2]

  alias ExOAPI.Parser.V3.Context

  @list_of_fields [
    :title,
    :field_name,
    :multiple_of,
    :maximum,
    :exclusive_maximum,
    :minimum,
    :exclusive_minimum,
    :max_length,
    :min_length,
    :pattern,
    :max_items,
    :min_items,
    :unique_items,
    :max_properties,
    :min_properties,
    :required,
    :enum,
    :type,
    :properties,
    :additional_properties,
    :description,
    :format,
    # default
    :nullable,
    :read_only,
    :write_only,
    :deprecated,
    :ref
  ]

  @translations [
    {"multipleOf", "multiple_of"},
    {"exclusiveMaximum", "exclusive_maximum"},
    {"exclusiveMinimum", "exclusive_minimum"},
    {"maxLength", "max_length"},
    {"minLength", "min_length"},
    {"maxItems", "max_items"},
    {"minItems", "min_items"},
    {"uniqueItems", "unique_items"},
    {"maxProperties", "max_properties"},
    {"minProperties", "min_properties"},
    {"allOf", "all_of", []},
    {"anyOf", "any_of", []},
    {"oneOf", "one_of", []},
    {"additionalProperties", "additional_properties"},
    {"readOnly", "read_only"},
    {"writeOnly", "write_only"},
    {"externalDocs", "external_docs"},
    {"$ref", "ref"}
  ]

  @primary_key false

  typed_embedded_schema do
    field(:title, :string)
    field(:field_name, ExOAPI.EctoTypes.FieldAtom)
    field(:multiple_of, :integer)
    field(:maximum, :integer)
    field(:exclusive_maximum, :boolean, default: false)
    field(:minimum, :integer)
    field(:exclusive_minimum, :boolean, default: false)
    field(:max_length, :integer)
    field(:min_length, :integer)
    field(:pattern, :string)
    field(:max_items, :integer)
    field(:min_items, :integer)
    field(:unique_items, :boolean, default: false)
    field(:max_properties, :integer)
    field(:min_properties, :integer)
    field(:required, {:array, ExOAPI.EctoTypes.FieldAtom})

    field(
      :enum,
      {:array, ExOAPI.EctoTypes.Maybe},
      types: [
        {:string, &__MODULE__.maybe_string/2},
        {:boolean, &__MODULE__.maybe_boolean/2}
      ]
    )

    field(:type, ExOAPI.EctoTypes.SchemaType)

    field(:properties, Context.Schema.Map)

    field(:additional_properties, ExOAPI.EctoTypes.Maybe,
      types: [
        {:boolean, &__MODULE__.maybe_boolean/2},
        {__MODULE__, &__MODULE__.maybe_schema/2}
      ]
    )

    embeds_many(:all_of, Context.Schema)
    embeds_many(:any_of, Context.Schema)
    embeds_many(:one_of, Context.Schema)

    embeds_one(:not, Context.Schema)
    embeds_one(:items, Context.Schema)

    embeds_one(:xml, Context.XML)
    embeds_one(:external_docs, Context.ExternalDoc)
    embeds_one(:discriminator, Context.Discriminator)

    field(:description, :string)
    field(:format, :string)
    # default
    field(:nullable, :boolean)
    field(:read_only, :boolean)
    field(:write_only, :boolean)
    field(:deprecated, :boolean)
    field(:ref, :string)
  end

  def map_cast(%__MODULE__{} = struct, params, k) do
    with {:ok, translated} <- translate(params, @translations) do
      previous_toggle = Context.get_and_set_schema_culling(false)

      struct
      |> cast(translated, @list_of_fields)
      |> cast_embed(:all_of, with: &__MODULE__.map_cast/2)
      |> cast_embed(:any_of, with: &__MODULE__.map_cast/2)
      |> cast_embed(:one_of, with: &__MODULE__.map_cast/2)
      |> cast_embed(:not, with: &__MODULE__.map_cast/2)
      |> cast_embed(:items, with: &__MODULE__.map_cast/2)
      |> cast_embed(:xml, with: &Context.XML.map_cast/2)
      |> cast_embed(:external_docs, with: &Context.ExternalDoc.map_cast/2)
      |> cast_embed(:discriminator, with: &Context.Discriminator.map_cast/2)
      |> maybe_add_ref(k)
      |> Context.toggle_schema_culling(previous_toggle)
    end
  end

  def map_cast(params), do: map_cast(%__MODULE__{}, params, nil)
  def map_cast(%__MODULE__{} = struct, params), do: map_cast(struct, params, nil)
  def map_cast(params, k), do: map_cast(%__MODULE__{}, params, k)

  def maybe_boolean(data, _params), do: Ecto.Type.cast(:boolean, data)
  def maybe_string(data, _params), do: Ecto.Type.cast(:string, data)

  def maybe_schema(data, _params, k \\ nil) do
    data
    |> map_cast(k)
    |> apply_action(:insert)
  end

  def maybe_add_ref(%Ecto.Changeset{} = changeset, k) do
    case get_field(changeset, :ref, nil) do
      nil ->
        changeset

      ref ->
        Context.put_ref(k, ref)
        changeset
    end
  end
end