Page MenuHomePhorge

No OneTemporary

Size
89 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/open_api_spex/cast/all_of.ex b/lib/open_api_spex/cast/all_of.ex
index a536f65..cc00b7f 100644
--- a/lib/open_api_spex/cast/all_of.ex
+++ b/lib/open_api_spex/cast/all_of.ex
@@ -1,84 +1,92 @@
defmodule OpenApiSpex.Cast.AllOf do
@moduledoc false
alias OpenApiSpex.Cast
alias OpenApiSpex.Schema
def cast(ctx), do: cast_all_of(ctx, nil)
defp cast_all_of(%{schema: %{allOf: [%Schema{type: :array} = schema | remaining]}} = ctx, acc)
when is_list(acc) or acc == nil do
# Since we parse a multi-type array, acc has to be a list or nil
acc = acc || []
case Cast.cast(%{ctx | schema: schema}) do
{:ok, value} when is_list(value) ->
# Since the cast for the list didn't result in a cast error,
# we do not proceed the values through the remaining schemas
{:ok, Enum.concat(acc, value)}
{:error, errors} ->
with {:ok, cleaned_ctx} <- reject_error_values(ctx, errors) do
case Cast.cast(cleaned_ctx) do
{:ok, cleaned_values} ->
new_ctx = put_in(ctx.schema.allOf, remaining)
new_ctx = update_in(new_ctx.value, fn values -> values -- cleaned_ctx.value end)
cast_all_of(new_ctx, Enum.concat(acc, cleaned_values))
end
else
_ -> Cast.error(ctx, {:all_of, to_string(schema.title || schema.type)})
end
end
end
defp cast_all_of(%{schema: %{allOf: [%Schema{} = schema | remaining]}} = ctx, acc) do
- schema = OpenApiSpex.resolve_schema(schema, ctx.schemas)
- relaxed_schema = %{schema | additionalProperties: true}
+ relaxed_schema = %{schema | additionalProperties: true, "x-struct": nil}
new_ctx = put_in(ctx.schema.allOf, remaining)
case Cast.cast(%{ctx | schema: relaxed_schema}) do
{:ok, value} when is_map(value) ->
- cast_all_of(new_ctx, Map.merge(acc || %{}, value))
+ # Complex allOf Schema
- {:ok, value} when acc == nil ->
- # primitive value with no previous (valid) casts -> return
- {:ok, value}
+ # reject all "additionalProperties"
+ acc =
+ value
+ |> Enum.reject(fn {k, _} -> is_binary(k) end)
+ |> Enum.concat(acc || %{})
+ |> Map.new()
- {:error, _} when remaining == [] ->
- # Since no schema is left to parse the remaining values, we return a error
- Cast.error(ctx, {:all_of, to_string(relaxed_schema.title || relaxed_schema.type)})
+ cast_all_of(new_ctx, acc)
+
+ {:ok, value} ->
+ # allOf definitions with primitives are a little bit strange..., we just return the cast for the first Schema,
+ # but validate the values against every other schema as well, since the value must be compatible with all Schemas
+ cast_all_of(new_ctx, acc || value)
{:error, _} ->
- # in case the cast results in a error, we just skip this schema
- cast_all_of(new_ctx, acc)
+ # Since in a allOf Schema, every
+ Cast.error(ctx, {:all_of, to_string(relaxed_schema.title || relaxed_schema.type)})
end
end
defp cast_all_of(%{schema: %{allOf: [schema | remaining]}} = ctx, result) do
schema = OpenApiSpex.resolve_schema(schema, ctx.schemas)
cast_all_of(%{ctx | schema: %{allOf: [schema | remaining]}}, result)
end
defp cast_all_of(%{schema: schema} = ctx, nil) do
Cast.error(ctx, {:all_of, to_string(schema.title || schema.type)})
end
+ defp cast_all_of(%{schema: %{allOf: [], "x-struct": module}}, acc) when not is_nil(module),
+ do: struct(module, acc)
+
defp cast_all_of(%{schema: %{allOf: []}}, acc) do
# All values have been casted against the allOf schemas - return accumulator
{:ok, acc}
end
defp reject_error_values(%{value: values} = ctx, [%{reason: :invalid_type} = error | tail]) do
new_values = List.delete(values, error.value)
reject_error_values(%{ctx | value: new_values}, tail)
end
defp reject_error_values(ctx, []) do
# All errors should now be resolved for the current schema
{:ok, ctx}
end
defp reject_error_values(_ctx, errors) do
# Some errors couldn't be resolved, we break and return the remaining errors
errors
end
end
diff --git a/lib/open_api_spex/cast/discriminator.ex b/lib/open_api_spex/cast/discriminator.ex
index dc4d42d..663a8fa 100644
--- a/lib/open_api_spex/cast/discriminator.ex
+++ b/lib/open_api_spex/cast/discriminator.ex
@@ -1,102 +1,101 @@
defmodule OpenApiSpex.Cast.Discriminator do
@moduledoc """
Defines the `OpenApiSpex.Discriminator.t` type.
"""
alias OpenApiSpex.Cast
@enforce_keys :propertyName
defstruct [
:propertyName,
:mapping
]
@typedoc """
[Discriminator Object](https://swagger.io/specification/#discriminatorObject)
When request bodies or response payloads may be one of a number of different schemas,
a discriminator object can be used to aid in serialization, deserialization, and validation.
The discriminator is a specific object in a schema which is used to inform the consumer of the
specification of an alternative schema based on the value associated with it.
A discriminator requires a composite key be set on the schema:
* `allOf`
* `oneOf`
* `anyOf`
"""
@type t :: %__MODULE__{
propertyName: String.t(),
mapping: %{String.t() => String.t()} | nil
}
def cast(ctx) do
case cast_discriminator(ctx) do
{:ok, result} -> {:ok, result}
error -> error
end
end
defp cast_discriminator(%_{value: value, schema: schema} = ctx) do
{discriminator_property, mappings} = discriminator_details(schema)
case Map.pop(value, "#{discriminator_property}") do
{"", _} ->
error(:no_value_for_discriminator, ctx)
- {discriminator_value, castable_value} ->
+ {discriminator_value, _castable_value} ->
# The cast specified by the composite key (allOf, anyOf, oneOf) MUST succeed
# or return an error according to the Open API Spec.
composite_ctx = %{
ctx
- | value: castable_value,
- schema: %{schema | discriminator: nil},
+ | schema: %{schema | discriminator: nil},
path: ["#{discriminator_property}" | ctx.path]
}
cast_composition(composite_ctx, ctx, discriminator_value, mappings)
end
end
defp cast_composition(composite_ctx, ctx, discriminator_value, mappings) do
with {composite_schemas, {:ok, _}} <- cast_composition(composite_ctx),
%{} = schema <-
find_discriminator_schema(discriminator_value, mappings, composite_schemas) do
Cast.cast(%{composite_ctx | schema: schema})
else
nil -> error(:invalid_discriminator_value, ctx)
other -> other
end
end
defp cast_composition(%_{schema: %{anyOf: schemas, discriminator: nil}} = ctx)
when is_list(schemas),
do: {schemas, Cast.cast(ctx)}
defp cast_composition(%_{schema: %{allOf: schemas, discriminator: nil}} = ctx)
when is_list(schemas),
do: {schemas, Cast.cast(ctx)}
defp cast_composition(%_{schema: %{oneOf: schemas, discriminator: nil}} = ctx)
when is_list(schemas),
do: {schemas, Cast.cast(ctx)}
defp find_discriminator_schema(discriminator, mappings = %{}, schemas) do
with {:ok, "#/components/schemas/" <> name} <- Map.fetch(mappings, discriminator) do
find_discriminator_schema(name, nil, schemas)
else
:error -> find_discriminator_schema(discriminator, nil, schemas)
end
end
defp find_discriminator_schema(discriminator, _, schemas) do
Enum.find(schemas, &Kernel.==(&1.title, discriminator))
end
defp discriminator_details(%{discriminator: %{propertyName: property_name, mapping: mappings}}),
do: {String.to_existing_atom(property_name), mappings}
defp error(message, %{schema: %{discriminator: %{propertyName: property}}} = ctx) do
Cast.error(ctx, {message, property})
end
end
diff --git a/lib/open_api_spex/cast/one_of.ex b/lib/open_api_spex/cast/one_of.ex
index b5a7623..92d89f5 100644
--- a/lib/open_api_spex/cast/one_of.ex
+++ b/lib/open_api_spex/cast/one_of.ex
@@ -1,48 +1,51 @@
defmodule OpenApiSpex.Cast.OneOf do
@moduledoc false
alias OpenApiSpex.Cast
+ alias OpenApiSpex.Schema
def cast(%_{schema: %{type: _, oneOf: []}} = ctx) do
error(ctx, [])
end
def cast(%{schema: %{type: _, oneOf: schemas}} = ctx) do
castable_schemas =
Enum.reduce(schemas, {[], 0}, fn schema, {results, count} ->
schema = OpenApiSpex.resolve_schema(schema, ctx.schemas)
case Cast.cast(%{ctx | schema: %{schema | anyOf: nil}}) do
{:ok, value} -> {[{:ok, value, schema} | results], count + 1}
_ -> {results, count}
end
end)
case castable_schemas do
- {[{:ok, value, _schema}], 1} -> {:ok, value}
+ {[{:ok, %_{} = value, _}], 1} -> {:ok, value}
+ {[{:ok, value, %Schema{"x-struct": nil}}], 1} -> {:ok, value}
+ {[{:ok, value, %Schema{"x-struct": module}}], 1} -> {:ok, struct(module, value)}
{failed_schemas, _count} -> error(ctx, failed_schemas)
end
end
## Private functions
defp error(ctx, failed_schemas) do
Cast.error(ctx, {:one_of, error_message(failed_schemas)})
end
defp error_message([]) do
"[] (no schemas provided)"
end
defp error_message(failed_schemas) do
for {:ok, _value, schema} <- failed_schemas do
case schema do
%{title: title, type: type} when not is_nil(title) ->
"Schema(title: #{inspect(title)}, type: #{inspect(type)})"
%{type: type} ->
"Schema(type: #{inspect(type)})"
end
end
|> Enum.join(", ")
end
end
diff --git a/lib/open_api_spex/plug/json_render_error.ex b/lib/open_api_spex/plug/json_render_error.ex
index ef98936..711c099 100644
--- a/lib/open_api_spex/plug/json_render_error.ex
+++ b/lib/open_api_spex/plug/json_render_error.ex
@@ -1,39 +1,38 @@
defmodule OpenApiSpex.Plug.JsonRenderError do
@behaviour Plug
alias Plug.Conn
alias OpenApiSpex.OpenApi
@impl Plug
def init(opts), do: opts
@impl Plug
def call(conn, errors) when is_list(errors) do
response = %{
errors: Enum.map(errors, &render_error/1)
}
json = OpenApi.json_encoder().encode!(response)
conn
|> Conn.put_resp_content_type("application/json")
|> Conn.send_resp(422, json)
end
def call(conn, reason) do
call(conn, [reason])
end
defp render_error(error) do
- path = error.path |> Enum.map(&to_string/1) |> Path.join()
- pointer = "/" <> path
+ pointer = OpenApiSpex.path_to_string(error)
%{
title: "Invalid value",
source: %{
pointer: pointer
},
message: to_string(error)
}
end
end
diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex
index 4920d16..4cb5e2a 100644
--- a/lib/open_api_spex/schema.ex
+++ b/lib/open_api_spex/schema.ex
@@ -1,873 +1,862 @@
defmodule OpenApiSpex.Schema do
@moduledoc """
Defines the `OpenApiSpex.Schema.t` type and operations for casting and validating against a schema.
The `OpenApiSpex.schema` macro can be used to declare schemas with an associated struct and JSON Encoder.
## Examples
defmodule MyApp.Schemas do
defmodule EmailString do
@behaviour OpenApiSpex.Schema
def schema do
%OpenApiSpex.Schema {
title: "EmailString",
type: :string,
format: :email
}
end
end
defmodule Person do
require OpenApiSpex
alias OpenApiSpex.{Reference, Schema}
OpenApiSpex.schema(%{
type: :object,
required: [:name],
properties: %{
name: %Schema{type: :string},
address: %Reference{"$ref": "#/components/schemas/Address"},
age: %Schema{type: :integer, format: :int32, minimum: 0}
}
})
end
defmodule StringDictionary do
@behaviour OpenApiSpex.Schema
def schema() do
%OpenApiSpex.Schema{
type: :object,
additionalProperties: %{
type: :string
}
}
end
end
defmodule Pet do
require OpenApiSpex
alias OpenApiSpex.{Schema, Discriminator}
OpenApiSpex.schema(%{
title: "Pet",
type: :object,
discriminator: %Discriminator{
propertyName: "petType"
},
properties: %{
name: %Schema{type: :string},
petType: %Schema{type: :string}
},
required: [:name, :petType]
})
end
defmodule Cat do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "Cat",
type: :object,
description: "A representation of a cat. Note that `Cat` will be used as the discriminator value.",
allOf: [
Pet,
%Schema{
type: :object,
properties: %{
huntingSkill: %Schema{
type: :string,
description: "The measured skill for hunting",
default: "lazy",
enum: ["clueless", "lazy", "adventurous", "aggresive"]
}
},
required: [:huntingSkill]
}
]
})
end
defmodule Dog do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
type: :object,
title: "Dog",
description: "A representation of a dog. Note that `Dog` will be used as the discriminator value.",
allOf: [
Pet,
%Schema {
type: :object,
properties: %{
packSize: %Schema{
type: :integer,
format: :int32,
description: "the size of the pack the dog is from",
default: 0,
minimum: 0
}
},
required: [
:packSize
]
}
]
})
end
end
"""
alias OpenApiSpex.{
Schema,
Reference,
Discriminator,
Xml,
ExternalDocumentation
}
@doc """
A module implementing the `OpenApiSpex.Schema` behaviour should export a `schema/0` function
that produces an `OpenApiSpex.Schema` struct.
"""
@callback schema() :: t
defstruct [
:title,
:multipleOf,
:maximum,
:exclusiveMaximum,
:minimum,
:exclusiveMinimum,
:maxLength,
:minLength,
:pattern,
:maxItems,
:minItems,
:uniqueItems,
:maxProperties,
:minProperties,
:required,
:enum,
:type,
:allOf,
:oneOf,
:anyOf,
:not,
:items,
:properties,
:additionalProperties,
:description,
:format,
:default,
:nullable,
:discriminator,
:readOnly,
:writeOnly,
:xml,
:externalDocs,
:example,
:deprecated,
:"x-struct"
]
@typedoc """
[Schema Object](https://swagger.io/specification/#schemaObject)
The Schema Object allows the definition of input and output data types.
These types can be objects, but also primitives and arrays.
This object is an extended subset of the JSON Schema Specification Wright Draft 00.
## Example
alias OpenApiSpex.Schema
%Schema{
title: "User",
type: :object,
properties: %{
id: %Schema{type: :integer, minimum: 1},
name: %Schema{type: :string, pattern: "[a-zA-Z][a-zA-Z0-9_]+"},
email: %Scheam{type: :string, format: :email},
last_login: %Schema{type: :string, format: :"date-time"}
},
required: [:name, :email],
example: %{
"name" => "joe",
"email" => "joe@gmail.com"
}
}
"""
@type t :: %__MODULE__{
title: String.t() | nil,
multipleOf: number | nil,
maximum: number | nil,
exclusiveMaximum: boolean | nil,
minimum: number | nil,
exclusiveMinimum: boolean | nil,
maxLength: integer | nil,
minLength: integer | nil,
pattern: String.t() | Regex.t() | nil,
maxItems: integer | nil,
minItems: integer | nil,
uniqueItems: boolean | nil,
maxProperties: integer | nil,
minProperties: integer | nil,
required: [atom] | nil,
enum: [String.t()] | nil,
type: data_type | nil,
allOf: [Schema.t() | Reference.t() | module] | nil,
oneOf: [Schema.t() | Reference.t() | module] | nil,
anyOf: [Schema.t() | Reference.t() | module] | nil,
not: Schema.t() | Reference.t() | module | nil,
items: Schema.t() | Reference.t() | module | nil,
properties: %{atom => Schema.t() | Reference.t() | module} | nil,
additionalProperties: boolean | Schema.t() | Reference.t() | module | nil,
description: String.t() | nil,
format: String.t() | atom | nil,
default: any,
nullable: boolean | nil,
discriminator: Discriminator.t() | nil,
readOnly: boolean | nil,
writeOnly: boolean | nil,
xml: Xml.t() | nil,
externalDocs: ExternalDocumentation.t() | nil,
example: any,
deprecated: boolean | nil,
"x-struct": module | nil
}
@typedoc """
The basic data types supported by openapi.
[Reference](https://swagger.io/docs/specification/data-models/data-types/)
"""
@type data_type :: :string | :number | :integer | :boolean | :array | :object
@typedoc """
Global schemas lookup by name.
"""
@type schemas :: %{String.t() => t()}
@doc """
Cast a simple value to the elixir type defined by a schema.
By default, object types are cast to maps, however if the "x-struct" attribute is set in the schema,
the result will be constructed as an instance of the given struct type.
## Examples
iex> OpenApiSpex.Schema.cast(%Schema{type: :integer}, "123", %{})
{:ok, 123}
iex> {:ok, dt = %DateTime{}} = OpenApiSpex.Schema.cast(%Schema{type: :string, format: :"date-time"}, "2018-04-02T13:44:55Z", %{})
...> dt |> DateTime.to_iso8601()
"2018-04-02T13:44:55Z"
## Casting Polymorphic Schemas
Schemas using `discriminator`, `allOf`, `oneOf`, `anyOf` are cast using the following rules:
- If a `discriminator` is present, cast the properties defined in the base schema, then
cast the result using the schema identified by the discriminator. To avoid infinite recursion,
the discriminator is only dereferenced if the discriminator property has not already been cast.
- Cast the properties using each schema listing in `allOf`. When a property is defined in
multiple `allOf` schemas, it will be cast using the first schema listed containing the property.
- Cast the value using each schema listed in `oneOf`, stopping as soon as a sucessful cast is made.
- Cast the value using each schema listed in `anyOf`, stopping as soon as a succesful cast is made.
"""
def cast(%Schema{type: :boolean}, value, _schemas) when is_boolean(value), do: {:ok, value}
def cast(%Schema{type: :boolean}, value, _schemas) when is_binary(value) do
case value do
"true" -> {:ok, true}
"false" -> {:ok, false}
_ -> {:error, "Invalid boolean: #{inspect(value)}"}
end
end
def cast(%Schema{type: :integer}, value, _schemas) when is_integer(value), do: {:ok, value}
def cast(%Schema{type: :integer}, value, _schemas) when is_binary(value) do
case Integer.parse(value) do
{i, ""} -> {:ok, i}
_ -> {:error, :bad_integer}
end
end
def cast(%Schema{type: :number}, value, _schemas) when is_float(value), do: {:ok, value}
def cast(%Schema{type: :number}, value, _schemas) when is_integer(value), do: {:ok, value / 1}
def cast(%Schema{type: :number}, value, _schemas) when is_binary(value) do
case Float.parse(value) do
{x, ""} -> {:ok, x}
_ -> {:error, :bad_float}
end
end
def cast(%Schema{type: :string, format: :"date-time"}, value, _schemas) when is_binary(value) do
case DateTime.from_iso8601(value) do
{:ok, date_time = %DateTime{}, _offset} -> {:ok, date_time}
error = {:error, _reason} -> error
end
end
def cast(%Schema{type: :string, format: :date}, value, _schemas) when is_binary(value) do
case Date.from_iso8601(value) do
{:ok, date = %Date{}} -> {:ok, date}
error = {:error, _reason} -> error
end
end
def cast(%Schema{type: :string, format: :binary}, %Plug.Upload{} = value, _schemas) do
{:ok, value}
end
def cast(%Schema{type: :string}, value, _schemas) when is_binary(value), do: {:ok, value}
def cast(%Schema{type: :array, items: nil}, value, _schemas) when is_list(value),
do: {:ok, value}
def cast(%Schema{type: :array}, [], _schemas), do: {:ok, []}
def cast(schema = %Schema{type: :array, items: items_schema}, [x | rest], schemas) do
with {:ok, x_cast} <- cast(items_schema, x, schemas),
{:ok, rest_cast} <- cast(schema, rest, schemas) do
{:ok, [x_cast | rest_cast]}
end
end
def cast(%Schema{type: :array}, value, _schemas) when not is_list(value) do
{:error, "Invalid array: #{inspect(value)}"}
end
def cast(
schema = %Schema{type: :object, discriminator: discriminator = %{}},
value = %{},
schemas
) do
discriminator_property = String.to_existing_atom(discriminator.propertyName)
already_cast? =
if Map.has_key?(value, discriminator_property) do
{:error, :already_cast}
else
:ok
end
with :ok <- already_cast?,
{:ok, partial_cast} <-
cast(%Schema{type: :object, properties: schema.properties}, value, schemas),
{:ok, derived_schema} <- Discriminator.resolve(discriminator, value, schemas),
{:ok, result} <- cast(derived_schema, partial_cast, schemas),
- {:ok, struct} <- make_struct(result, schema) do
+ {:ok, struct} <- make_struct(result, derived_schema) do
{:ok, struct}
else
{:error, :already_cast} -> {:ok, value}
{:error, reason} -> {:error, reason}
end
end
- def cast(schema = %Schema{type: :object, allOf: [first | rest]}, value = %{}, schemas) do
- with {:ok, cast_first} <- cast(first, value, schemas),
- {:ok, result} <- cast(%{schema | allOf: rest}, cast_first, schemas) do
- {:ok, result}
- else
- {:error, reason} -> {:error, reason}
- end
- end
+ def cast(schema = %Schema{type: :object, allOf: all_of}, value = %{}, schemas)
+ when is_list(all_of),
+ do: OpenApiSpex.Cast.cast(schema, value, schemas)
def cast(schema = %Schema{type: :object, allOf: []}, value = %{}, schemas) do
cast(%{schema | allOf: nil}, value, schemas)
end
- def cast(schema = %Schema{oneOf: [first | rest]}, value, schemas) do
- case cast(first, value, schemas) do
- {:ok, result} ->
- {:ok, result}
-
- {:error, _reason} ->
- cast(%{schema | oneOf: rest}, value, schemas)
- end
- end
+ def cast(schema = %Schema{oneOf: one_of}, value, schemas)
+ when is_list(one_of),
+ do: OpenApiSpex.Cast.cast(schema, value, schemas)
def cast(%Schema{oneOf: []}, _value, _schemas) do
{:error, "Failed to cast to any schema in oneOf"}
end
def cast(schema = %Schema{anyOf: [first | rest]}, value, schemas) do
case cast(first, value, schemas) do
{:ok, result} ->
cast(%{schema | anyOf: nil}, result, schemas)
{:error, _reason} ->
cast(%{schema | anyOf: rest}, value, schemas)
end
end
def cast(%Schema{anyOf: []}, _value, _schemas) do
{:error, "Failed to cast to any schema in anyOf"}
end
def cast(schema = %Schema{type: :object}, value, schemas) when is_map(value) do
schema = %{schema | properties: schema.properties || %{}}
{regular_properties, others} =
value
|> no_struct()
|> Enum.split_with(fn {k, _v} -> is_binary(k) end)
with {:ok, props} <- cast_properties(schema, regular_properties, schemas),
{:ok, struct} <- Map.new(others ++ props) |> make_struct(schema) do
{:ok, struct}
end
end
def cast(ref = %Reference{}, val, schemas),
do: cast(Reference.resolve_schema(ref, schemas), val, schemas)
def cast(_additionalProperties = false, val, _schemas) do
{:error, "Unexpected field with value #{inspect(val)}"}
end
def cast(_additionalProperties, val, _schemas), do: {:ok, val}
defp make_struct(val = %_{}, _), do: {:ok, val}
defp make_struct(val, %{"x-struct": nil}), do: {:ok, val}
defp make_struct(val, %{"x-struct": mod}) do
keys = mod.schema |> OpenApiSpex.Schema.properties() |> Keyword.keys()
if Map.keys(val) -- keys == [] do
{:ok, struct(mod, val)}
else
{:error, :bad_structure}
end
end
defp no_struct(val), do: Map.delete(val, :__struct__)
@spec cast_properties(Schema.t(), list, %{String.t() => Schema.t()}) ::
{:ok, list} | {:error, String.t()}
defp cast_properties(%Schema{}, [], _schemas), do: {:ok, []}
defp cast_properties(object_schema = %Schema{}, [{key, value} | rest], schemas) do
{name, schema} =
Enum.find(
object_schema.properties,
{key, object_schema.additionalProperties},
fn {name, _schema} -> to_string(name) == to_string(key) end
)
with {:ok, new_value} <- cast(schema, value, schemas),
{:ok, cast_tail} <- cast_properties(object_schema, rest, schemas) do
{:ok, [{name, new_value} | cast_tail]}
end
end
@doc ~S"""
Validate a value against a Schema.
This expects that the value has already been `cast` to the appropriate data type.
## Examples
iex> OpenApiSpex.Schema.validate(%OpenApiSpex.Schema{type: :integer, minimum: 5}, 3, %{})
{:error, "#: 3 is smaller than minimum 5"}
iex> OpenApiSpex.Schema.validate(%OpenApiSpex.Schema{type: :string, pattern: "(.*)@(.*)"}, "joe@gmail.com", %{})
:ok
iex> OpenApiSpex.Schema.validate(%OpenApiSpex.Schema{type: :string, pattern: "(.*)@(.*)"}, "joegmail.com", %{})
{:error, "#: Value \"joegmail.com\" does not match pattern: (.*)@(.*)"}
"""
@spec validate(Schema.t() | Reference.t(), any, %{String.t() => Schema.t() | Reference.t()}) ::
:ok | {:error, String.t()}
def validate(schema, val, schemas), do: validate(schema, val, "#", schemas)
@spec validate(Schema.t() | Reference.t(), any, String.t(), %{
String.t() => Schema.t() | Reference.t()
}) :: :ok | {:error, String.t()}
def validate(ref = %Reference{}, val, path, schemas),
do: validate(Reference.resolve_schema(ref, schemas), val, path, schemas)
def validate(%Schema{nullable: true}, nil, _path, _schemas), do: :ok
def validate(%Schema{type: type}, nil, path, _schemas) when not is_nil(type) do
{:error, "#{path}: null value where #{type} expected"}
end
def validate(schema = %Schema{anyOf: valid_schemas}, value, path, schemas)
when is_list(valid_schemas) do
if Enum.any?(valid_schemas, &(validate(&1, value, path, schemas) == :ok)) do
validate(%{schema | anyOf: nil}, value, path, schemas)
else
{:error, "#{path}: Failed to validate against any schema"}
end
end
def validate(schema = %Schema{oneOf: valid_schemas}, value, path, schemas)
when is_list(valid_schemas) do
case Enum.count(valid_schemas, &(validate(&1, value, path, schemas) == :ok)) do
1 -> validate(%{schema | oneOf: nil}, value, path, schemas)
0 -> {:error, "#{path}: Failed to validate against any schema"}
other -> {:error, "#{path}: Validated against #{other} schemas when only one expected"}
end
end
def validate(schema = %Schema{allOf: required_schemas}, value, path, schemas)
when is_list(required_schemas) do
required_schemas
|> Enum.map(&validate(&1, value, path, schemas))
|> Enum.reject(&(&1 == :ok))
|> Enum.map(fn {:error, msg} -> msg end)
|> case do
[] -> validate(%{schema | allOf: nil}, value, path, schemas)
errors -> {:error, Enum.join(errors, "\n")}
end
end
def validate(schema = %Schema{not: not_schema}, value, path, schemas)
when not is_nil(not_schema) do
case validate(not_schema, value, path, schemas) do
{:error, _} -> validate(%{schema | not: nil}, value, path, schemas)
:ok -> {:error, "#{path}: Value is valid for schema given in `not`"}
end
end
def validate(%Schema{enum: options = [_ | _]}, value, path, _schemas) do
case Enum.member?(options, value) do
true ->
:ok
_ ->
{:error, "#{path}: Value not in enum: #{inspect(value)}"}
end
end
def validate(schema = %Schema{type: :integer}, value, path, _schemas) when is_integer(value) do
validate_number_types(schema, value, path)
end
def validate(schema = %Schema{type: :number}, value, path, _schemas) when is_number(value) do
validate_number_types(schema, value, path)
end
def validate(schema = %Schema{type: :string}, value, path, _schemas) when is_binary(value) do
validate_string_types(schema, value, path)
end
def validate(%Schema{type: :string, format: :"date-time"}, %DateTime{}, _path, _schemas) do
:ok
end
def validate(%Schema{type: :string, format: :binary}, %Plug.Upload{}, _path, _schemas) do
:ok
end
def validate(%Schema{type: expected_type}, %DateTime{}, path, _schemas) do
{:error, "#{path}: invalid type DateTime where #{expected_type} expected"}
end
def validate(%Schema{type: :string, format: :date}, %Date{}, _path, _schemas) do
:ok
end
def validate(%Schema{type: expected_type}, %Date{}, path, _schemas) do
{:error, "#{path}: invalid type Date where #{expected_type} expected"}
end
def validate(%Schema{type: :boolean}, value, _path, _schemas) when is_boolean(value), do: :ok
def validate(schema = %Schema{type: :array}, value, path, schemas) when is_list(value) do
with :ok <- validate_max_items(schema, value, path),
:ok <- validate_min_items(schema, value, path),
:ok <- validate_unique_items(schema, value, path),
:ok <- validate_array_items(schema, value, {path, 0}, schemas) do
:ok
end
end
def validate(schema = %Schema{type: :object}, value = %{}, path, schemas) do
schema = %{schema | properties: schema.properties || %{}, required: schema.required || []}
with :ok <- validate_required_properties(schema, value, path),
:ok <- validate_max_properties(schema, value, path),
:ok <- validate_min_properties(schema, value, path),
:ok <-
validate_object_properties(
schema.properties,
MapSet.new(schema.required),
value,
path,
schemas
) do
:ok
end
end
def validate(%Schema{type: nil}, _value, _path, _schemas) do
# polymorphic schemas will terminate here after validating against anyOf/oneOf/allOf/not
:ok
end
def validate(%Schema{type: expected_type}, value, path, _schemas)
when not is_nil(expected_type) do
{:error, "#{path}: invalid type #{term_type(value)} where #{expected_type} expected"}
end
@spec term_type(term) :: data_type | nil | String.t()
defp term_type(v) when is_list(v), do: :array
defp term_type(v) when is_map(v), do: :object
defp term_type(v) when is_binary(v), do: :string
defp term_type(v) when is_boolean(v), do: :boolean
defp term_type(v) when is_integer(v), do: :integer
defp term_type(v) when is_number(v), do: :number
defp term_type(v) when is_nil(v), do: nil
defp term_type(v), do: inspect(v)
@spec validate_number_types(Schema.t(), number, String.t()) :: :ok | {:error, String.t()}
defp validate_number_types(schema, value, path) do
with :ok <- validate_multiple(schema, value, path),
:ok <- validate_maximum(schema, value, path),
:ok <- validate_minimum(schema, value, path) do
:ok
end
end
@spec validate_string_types(Schema.t(), String.t(), String.t()) :: :ok | {:error, String.t()}
defp validate_string_types(schema, value, path) do
with :ok <- validate_max_length(schema, value, path),
:ok <- validate_min_length(schema, value, path),
:ok <- validate_pattern(schema, value, path) do
:ok
end
end
@spec validate_multiple(Schema.t(), number, String.t()) :: :ok | {:error, String.t()}
defp validate_multiple(%{multipleOf: nil}, _, _), do: :ok
defp validate_multiple(%{multipleOf: n}, value, _) when round(value / n) * n == value, do: :ok
defp validate_multiple(%{multipleOf: n}, value, path),
do: {:error, "#{path}: #{value} is not a multiple of #{n}"}
@spec validate_maximum(Schema.t(), number, String.t()) :: :ok | {:error, String.t()}
defp validate_maximum(%{maximum: nil}, _val, _path), do: :ok
defp validate_maximum(%{maximum: n, exclusiveMaximum: true}, value, _path) when value < n,
do: :ok
defp validate_maximum(%{maximum: n, exclusiveMaximum: true}, value, path),
do: {:error, "#{path}: #{value} is larger than the exclusive maximum #{n}"}
defp validate_maximum(%{maximum: n}, value, _path) when value <= n, do: :ok
defp validate_maximum(%{maximum: n}, value, path),
do: {:error, "#{path}: #{value} is larger than maximum #{n}"}
@spec validate_minimum(Schema.t(), number, String.t()) :: :ok | {:error, String.t()}
defp validate_minimum(%{minimum: nil}, _val, _path), do: :ok
defp validate_minimum(%{minimum: n, exclusiveMinimum: true}, value, _path) when value > n,
do: :ok
defp validate_minimum(%{minimum: n, exclusiveMinimum: true}, value, path),
do: {:error, "#{path}: #{value} is smaller than the exclusive minimum #{n}"}
defp validate_minimum(%{minimum: n}, value, _path) when value >= n, do: :ok
defp validate_minimum(%{minimum: n}, value, path),
do: {:error, "#{path}: #{value} is smaller than minimum #{n}"}
@spec validate_max_length(Schema.t(), String.t(), String.t()) :: :ok | {:error, String.t()}
defp validate_max_length(%{maxLength: nil}, _val, _path), do: :ok
defp validate_max_length(%{maxLength: n}, value, path) do
case String.length(value) <= n do
true -> :ok
_ -> {:error, "#{path}: String length is larger than maxLength: #{n}"}
end
end
@spec validate_min_length(Schema.t(), String.t(), String.t()) :: :ok | {:error, String.t()}
defp validate_min_length(%{minLength: nil}, _val, _path), do: :ok
defp validate_min_length(%{minLength: n}, value, path) do
case String.length(value) >= n do
true -> :ok
_ -> {:error, "#{path}: String length is smaller than minLength: #{n}"}
end
end
@spec validate_pattern(Schema.t(), String.t(), String.t()) :: :ok | {:error, String.t()}
defp validate_pattern(%{pattern: nil}, _val, _path), do: :ok
defp validate_pattern(schema = %{pattern: regex}, val, path) when is_binary(regex) do
with {:ok, regex} <- Regex.compile(regex) do
validate_pattern(%{schema | pattern: regex}, val, path)
end
end
defp validate_pattern(%{pattern: regex = %Regex{}}, val, path) do
case Regex.match?(regex, val) do
true -> :ok
_ -> {:error, "#{path}: Value #{inspect(val)} does not match pattern: #{regex.source}"}
end
end
@spec validate_max_items(Schema.t(), list, String.t()) :: :ok | {:error, String.t()}
defp validate_max_items(%Schema{maxItems: nil}, _val, _path), do: :ok
defp validate_max_items(%Schema{maxItems: n}, value, _path) when length(value) <= n, do: :ok
defp validate_max_items(%Schema{maxItems: n}, value, path) do
{:error, "#{path}: Array length #{length(value)} is larger than maxItems: #{n}"}
end
@spec validate_min_items(Schema.t(), list, String.t()) :: :ok | {:error, String.t()}
defp validate_min_items(%Schema{minItems: nil}, _val, _path), do: :ok
defp validate_min_items(%Schema{minItems: n}, value, _path) when length(value) >= n, do: :ok
defp validate_min_items(%Schema{minItems: n}, value, path) do
{:error, "#{path}: Array length #{length(value)} is smaller than minItems: #{n}"}
end
@spec validate_unique_items(Schema.t(), list, String.t()) :: :ok | {:error, String.t()}
defp validate_unique_items(%Schema{uniqueItems: true}, value, path) do
unique_size =
value
|> MapSet.new()
|> MapSet.size()
case unique_size == length(value) do
true -> :ok
_ -> {:error, "#{path}: Array items must be unique"}
end
end
defp validate_unique_items(_schema, _value, _path), do: :ok
@spec validate_array_items(Schema.t(), list, {String.t(), integer}, %{String.t() => Schema.t()}) ::
:ok | {:error, String.t()}
defp validate_array_items(%Schema{type: :array, items: nil}, value, _path, _schemas)
when is_list(value),
do: :ok
defp validate_array_items(%Schema{type: :array}, [], _path, _schemas), do: :ok
defp validate_array_items(
schema = %Schema{type: :array, items: item_schema},
[x | rest],
{path, index},
schemas
) do
with :ok <- validate(item_schema, x, "#{path}/#{index}", schemas) do
validate_array_items(schema, rest, {path, index + 1}, schemas)
end
end
@spec validate_required_properties(Schema.t(), %{}, String.t()) :: :ok | {:error, String.t()}
defp validate_required_properties(%Schema{type: :object, required: nil}, _val, _path), do: :ok
defp validate_required_properties(%Schema{type: :object, required: required}, value = %{}, path) do
missing = required -- Map.keys(value)
case missing do
[] -> :ok
_ -> {:error, "#{path}: Missing required properties: #{inspect(missing)}"}
end
end
@spec validate_max_properties(Schema.t(), %{}, String.t()) :: :ok | {:error, String.t()}
defp validate_max_properties(%Schema{type: :object, maxProperties: nil}, _val, _path), do: :ok
defp validate_max_properties(%Schema{type: :object, maxProperties: n}, val, _path)
when map_size(val) <= n,
do: :ok
defp validate_max_properties(%Schema{type: :object, maxProperties: n}, val, path) do
{:error,
"#{path}: Object property count #{map_size(val)} is greater than maxProperties: #{n}"}
end
@spec validate_min_properties(Schema.t(), %{}, String.t()) :: :ok | {:error, String.t()}
defp validate_min_properties(%Schema{type: :object, minProperties: nil}, _val, _path), do: :ok
defp validate_min_properties(%Schema{type: :object, minProperties: n}, val, _path)
when map_size(val) >= n,
do: :ok
defp validate_min_properties(%Schema{type: :object, minProperties: n}, val, path) do
{:error, "#{path}: Object property count #{map_size(val)} is less than minProperties: #{n}"}
end
@spec validate_object_properties(Enumerable.t(), MapSet.t(), %{}, String.t(), %{
String.t() => Schema.t() | Reference.t()
}) :: :ok | {:error, String.t()}
defp validate_object_properties(properties = %{}, required, value = %{}, path, schemas = %{}) do
properties
|> Enum.filter(fn {name, _schema} -> Map.has_key?(value, name) end)
|> validate_object_properties(required, value, path, schemas)
end
defp validate_object_properties([], _required, _val, _path, _schemas), do: :ok
defp validate_object_properties(
[{name, schema} | rest],
required,
value = %{},
path,
schemas = %{}
) do
property_required = MapSet.member?(required, name)
property_value = Map.get(value, name)
property_path = "#{path}/#{name}"
with :ok <-
validate_object_property(
schema,
property_required,
property_value,
property_path,
schemas
),
:ok <- validate_object_properties(rest, required, value, path, schemas) do
:ok
end
end
defp validate_object_property(_schema, false, nil, _path, _schemas), do: :ok
defp validate_object_property(schema, _required, value, path, schemas) do
validate(schema, value, path, schemas)
end
@doc """
Get the names of all properties definied for a schema.
Includes all properties directly defined in the schema, and all schemas
included in the `allOf` list.
"""
def properties(schema = %Schema{type: :object, properties: properties = %{}}) do
for({name, property} <- properties, do: {name, default(property)}) ++
properties(%{schema | properties: nil})
end
def properties(%Schema{allOf: schemas}) when is_list(schemas) do
Enum.flat_map(schemas, &properties/1) |> Enum.uniq()
end
def properties(schema_module) when is_atom(schema_module) do
properties(schema_module.schema())
end
def properties(_), do: []
defp default(schema_module) when is_atom(schema_module), do: schema_module.schema().default
defp default(%{default: default}), do: default
end
diff --git a/test/cast/all_of_test.exs b/test/cast/all_of_test.exs
index 3c071e8..4724b2e 100644
--- a/test/cast/all_of_test.exs
+++ b/test/cast/all_of_test.exs
@@ -1,79 +1,82 @@
defmodule OpenApiSpex.CastAllOfTest do
use ExUnit.Case
alias OpenApiSpex.{Cast, Schema}
alias OpenApiSpex.Cast.{Error, AllOf}
alias OpenApiSpex.TestAssertions
defp cast(ctx), do: AllOf.cast(struct(Cast, ctx))
describe "cast/1" do
test "allOf" do
schema = %Schema{allOf: [%Schema{type: :integer}, %Schema{type: :string}]}
assert {:ok, 1} = cast(value: "1", schema: schema)
+ assert {:error, [error]} = cast(value: "one", schema: schema)
+ assert Error.message(error) ==
+ "Failed to cast value as integer. Value must be castable using `allOf` schemas listed."
end
test "allOf, uncastable schema" do
schema = %Schema{allOf: [%Schema{type: :integer}, %Schema{type: :string}]}
assert {:error, [error]} = cast(value: [:whoops], schema: schema)
assert Error.message(error) ==
- "Failed to cast value as string. Value must be castable using `allOf` schemas listed."
+ "Failed to cast value as integer. Value must be castable using `allOf` schemas listed."
schema_with_title = %Schema{allOf: [%Schema{title: "Age", type: :integer}]}
assert {:error, [error_with_schema_title]} =
cast(value: [:nopes], schema: schema_with_title)
assert Error.message(error_with_schema_title) ==
"Failed to cast value as Age. Value must be castable using `allOf` schemas listed."
end
test "a more sophisticated example" do
dog = %{"bark" => "woof", "pet_type" => "Dog"}
TestAssertions.assert_schema(dog, "Dog", OpenApiSpexTest.ApiSpec.spec())
end
test "allOf, for inheritance schema" do
schema = %Schema{
allOf: [
%Schema{
type: :object,
additionalProperties: true,
properties: %{
id: %Schema{
type: :string
}
}
},
%Schema{
type: :object,
additionalProperties: true,
properties: %{
bar: %Schema{
type: :string
}
}
}
]
}
value = %{id: "e30aee0f-dbda-40bd-9198-6cf609b8b640", bar: "foo"}
assert {:ok, %{id: "e30aee0f-dbda-40bd-9198-6cf609b8b640", bar: "foo"}} =
cast(value: value, schema: schema)
end
end
test "allOf, for multi-type array" do
schema = %Schema{
allOf: [
%Schema{type: :array, items: %Schema{type: :integer}},
%Schema{type: :array, items: %Schema{type: :boolean}},
%Schema{type: :array, items: %Schema{type: :string}}
]
}
value = ["Test #1", "2", "3", "4", "true", "Five!"]
assert {:ok, [2, 3, 4, true, "Test #1", "Five!"]} = cast(value: value, schema: schema)
end
end
diff --git a/test/cast/discriminator_test.exs b/test/cast/discriminator_test.exs
index 0ee7945..b479b8c 100644
--- a/test/cast/discriminator_test.exs
+++ b/test/cast/discriminator_test.exs
@@ -1,277 +1,280 @@
defmodule OpenApiSpex.CastDiscriminatorTest do
use ExUnit.Case
alias OpenApiSpex.{Cast, Schema}
alias OpenApiSpex.Cast.{Discriminator, Error}
# The discriminator we'll be using across the tests. Animal type was
# specifically chosen to help the examples be more clear since abstract
# objects can be difficult to grok in this context.
@discriminator "animal_type"
defp cast(ctx) do
Discriminator.cast(struct(Cast, ctx))
end
# This function is used for testing descriminators within nested
# schemas.
defp cast_cast(ctx) do
Cast.cast(struct(Cast, ctx))
end
# Maps an arbitrary string value to a schema
def build_discriminator_mapping(name, schema) do
%{name => schema_ref(schema)}
end
def schema_ref(%{title: title}), do: "#/components/schemas/#{title}"
def build_schema(title, properties) do
%Schema{
type: :object,
title: title,
properties: properties
}
end
def build_discriminator_schema(schemas, composite_keyword, property_name, mapppings \\ nil) do
%Schema{
type: :object,
discriminator: %{
propertyName: "#{property_name}",
mapping: mapppings
}
}
|> Map.put(composite_keyword, schemas)
end
setup do
dog_schema =
build_schema("Dog", %{
+ animal_type: %Schema{type: :string},
breed: %Schema{type: :string},
age: %Schema{type: :integer}
})
wolf_schema =
build_schema("Wolf", %{
+ animal_type: %Schema{type: :string},
breed: %Schema{type: :string, minLength: 5},
age: %Schema{type: :integer}
})
cat_schema =
build_schema("Cat", %{
+ animal_type: %Schema{type: :string},
breed: %Schema{type: :string},
lives: %Schema{type: :integer}
})
{:ok, schemas: %{dog: dog_schema, cat: cat_schema, wolf: wolf_schema}}
end
describe "cast/1" do
test "basics, anyOf", %{schemas: %{dog: dog, wolf: wolf}} do
# "animal_type" is the discriminator and the keys need to be strings.
input_value = %{@discriminator => "Dog", "breed" => "Pug", "age" => 1}
# Create the discriminator schema. Discriminators require a composite
# key be provided (`allOf`, `anyOf`, `oneOf`).
discriminator_schema =
build_discriminator_schema([dog, wolf], :anyOf, String.to_atom(@discriminator), nil)
# Note: We're expecting to getting atoms back, not strings
- expected = {:ok, %{age: 1, breed: "Pug"}}
+ expected = {:ok, %{age: 1, breed: "Pug", animal_type: "Dog"}}
assert cast(value: input_value, schema: discriminator_schema) == expected
end
test "basics, allOf", %{schemas: %{dog: dog, wolf: wolf}} do
# Wolf has a constraint on its "breed attribute" requiring the breed to have
# a minimum length of 5.
input_value = %{@discriminator => "Dog", "breed" => "Corgi", "age" => 1}
discriminator_schema =
build_discriminator_schema([dog, wolf], :allOf, String.to_atom(@discriminator), nil)
# Note: We're expecting to getting atoms back, not strings
- expected = {:ok, %{age: 1, breed: "Corgi"}}
+ expected = {:ok, %{age: 1, breed: "Corgi", animal_type: "Dog"}}
assert cast(value: input_value, schema: discriminator_schema) == expected
end
test "basics, oneOf", %{schemas: %{dog: dog, wolf: wolf}} do
# Wolf has a constraint on its "breed attribute" requiring the breed to have
# a minimum length of 5.
input_value = %{@discriminator => "Dog", "breed" => "Pug", "age" => 1}
discriminator_schema =
build_discriminator_schema([dog, wolf], :oneOf, String.to_atom(@discriminator), nil)
# Note: We're expecting to getting atoms back, not strings
- expected = {:ok, %{age: 1, breed: "Pug"}}
+ expected = {:ok, %{age: 1, breed: "Pug", animal_type: "Dog"}}
assert cast(value: input_value, schema: discriminator_schema) == expected
end
test "with mapping", %{schemas: %{dog: dog, cat: cat}} do
dog_schema_alias = "dag"
# "animal_type" is the discriminator and the keys need to be strings.
input_value = %{@discriminator => dog_schema_alias, "breed" => "Corgi", "age" => 1}
# Map the value 'dag' to the schema "Dog"
mapping = build_discriminator_mapping(dog_schema_alias, dog)
# Create the discriminator schema. Discriminators require a composite
# key be provided (`allOf`, `anyOf`, `oneOf`).
discriminator_schema =
build_discriminator_schema([dog, cat], :anyOf, String.to_atom(@discriminator), mapping)
# Note: We're expecting to getting atoms back, not strings
- expected = {:ok, %{age: 1, breed: "Corgi"}}
+ expected = {:ok, %{age: 1, breed: "Corgi", animal_type: "dag"}}
assert cast(value: input_value, schema: discriminator_schema) == expected
end
test "invalid property on discriminator schema", %{
schemas: %{dog: dog, wolf: wolf}
} do
# "Pug" will fail because the `breed` property for a Wolf must have a minimum
# length of 5.
input_value = %{@discriminator => "Wolf", "breed" => "Pug", "age" => 1}
# Create the discriminator schema. Discriminators require a composite
# key be provided (`allOf`, `anyOf`, `oneOf`).
discriminator_schema =
build_discriminator_schema([dog, wolf], :anyOf, String.to_atom(@discriminator), nil)
assert {:error, [error]} = cast(value: input_value, schema: discriminator_schema)
assert error.reason == :min_length
end
test "invalid discriminator value", %{schemas: %{dog: dog}} do
# Goats is not registered to any schema provided
empty_discriminator_value = %{@discriminator => "Goats", "breed" => "Corgi", "age" => 1}
discriminator_schema =
build_discriminator_schema([dog], :anyOf, String.to_atom(@discriminator), nil)
assert {:error, [error]} =
cast(
value: empty_discriminator_value,
schema: discriminator_schema
)
assert error.reason == :invalid_discriminator_value
end
test "empty discriminator value", %{schemas: %{dog: dog}} do
empty_discriminator_value = %{@discriminator => "", "breed" => "Corgi", "age" => 1}
discriminator_schema =
build_discriminator_schema([dog], :anyOf, String.to_atom(@discriminator), nil)
assert {:error, [error]} =
cast(
value: empty_discriminator_value,
schema: discriminator_schema
)
assert error.reason == :no_value_for_discriminator
end
test "nested, success", %{schemas: %{dog: dog, cat: cat}} do
# "animal_type" is the discriminator and the keys need to be strings.
input_value = %{"data" => %{@discriminator => "Dog", "breed" => "Corgi", "age" => 1}}
# Nested schema to better simulate use with JSON API (real world)
discriminator_schema = %Schema{
title: "Nested Skemuh",
type: :object,
properties: %{
data:
build_discriminator_schema([dog, cat], :anyOf, String.to_atom(@discriminator), nil)
}
}
# Note: We're expecting to getting atoms back, not strings
- expected = {:ok, %{data: %{age: 1, breed: "Corgi"}}}
+ expected = {:ok, %{data: %{age: 1, breed: "Corgi", animal_type: "Dog"}}}
assert expected == cast_cast(value: input_value, schema: discriminator_schema)
end
test "nested, with invalid property on discriminator schema", %{
schemas: %{dog: dog, wolf: wolf}
} do
# "animal_type" is the discriminator and the keys need to be strings.
input_value = %{"data" => %{@discriminator => "Wolf", "breed" => "Pug", "age" => 1}}
# Nested schema to better simulate use with JSON API (real world)
discriminator_schema = %Schema{
title: "Nested Skemuh",
type: :object,
properties: %{
data:
build_discriminator_schema([dog, wolf], :anyOf, String.to_atom(@discriminator), nil)
}
}
assert {:error, [error]} = cast_cast(value: input_value, schema: discriminator_schema)
# The format of the error path should be confirmed. This is just a guess.
assert Error.message_with_path(error) ==
"#/data/#{@discriminator}/breed: String length is smaller than minLength: 5"
end
test "without setting a composite key, raises compile time error" do
# While we're still specifying the composite key here, it'll be set to
# nil. E.g. %Schema{anyOf: nil, discriminator: %{...}}
discriminator_schema =
build_discriminator_schema(nil, :anyOf, String.to_atom(@discriminator), nil)
# We have to escape the map to unquote it later.
schema_as_map = Map.from_struct(discriminator_schema) |> Macro.escape()
# A module which will define our broken schema, and throw an error.
code =
quote do
defmodule DiscriminatorWihoutCompositeKey do
require OpenApiSpex
OpenApiSpex.schema(unquote(schema_as_map))
end
end
# Confirm we raise the error when we compile the code
assert_raise(OpenApiSpex.SchemaException, fn ->
Code.eval_quoted(code)
end)
end
# From the OAS discriminator docs:
#
# "[..] inline schema definitions, which do not have a given id,
# cannot be used in polymorphism."
test "discriminator schemas without titles, raise compile time error", %{
schemas: %{dog: dog, cat: cat}
} do
discriminator_schema =
build_discriminator_schema(
[cat, %{dog | title: nil}],
:anyOf,
String.to_atom(@discriminator),
nil
)
# We have to escape the map to unquote it later.
schema_as_map = Map.from_struct(discriminator_schema) |> Macro.escape()
# A module which will define our broken schema, and throw an error.
code =
quote do
defmodule DiscriminatorSchemaWithoutId do
require OpenApiSpex
OpenApiSpex.schema(unquote(schema_as_map))
end
end
# Confirm we raise the error when we compile the code
assert_raise(OpenApiSpex.SchemaException, fn ->
Code.eval_quoted(code)
end)
end
end
end
diff --git a/test/plug/cast_and_validate_test.exs b/test/plug/cast_test.exs
similarity index 71%
rename from test/plug/cast_and_validate_test.exs
rename to test/plug/cast_test.exs
index abb611c..07a544a 100644
--- a/test/plug/cast_and_validate_test.exs
+++ b/test/plug/cast_test.exs
@@ -1,159 +1,222 @@
defmodule OpenApiSpex.Plug.CastTest do
use ExUnit.Case
describe "query params - basics" do
test "Valid Param" do
conn =
:get
|> Plug.Test.conn("/api/users?validParam=true")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 200
end
test "Invalid value" do
conn =
:get
|> Plug.Test.conn("/api/users?validParam=123")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 422
end
test "Invalid Param" do
conn =
:get
|> Plug.Test.conn("/api/users?validParam=123")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 422
error_resp = Jason.decode!(conn.resp_body)
assert error_resp == %{
"errors" => [
%{
"message" => "Invalid boolean. Got: string",
"source" => %{"pointer" => "/validParam"},
"title" => "Invalid value"
}
]
}
end
test "with requestBody" do
body =
Jason.encode!(%{
phone_number: "123-456-789",
postal_address: "123 Lane St"
})
conn =
:post
|> Plug.Test.conn("/api/users/123/contact_info", body)
|> Plug.Conn.put_req_header("content-type", "application/json")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 200
end
end
describe "query params - param with custom error handling" do
test "Valid Param" do
conn =
:get
|> Plug.Test.conn("/api/custom_error_users?validParam=true")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 200
end
test "Invalid value" do
conn =
:get
|> Plug.Test.conn("/api/custom_error_users?validParam=123")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 400
end
test "Invalid Param" do
conn =
:get
|> Plug.Test.conn("/api/custom_error_users?validParam=123")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 400
assert conn.resp_body == "Invalid boolean. Got: string"
end
end
describe "body params" do
test "Valid Request" do
request_body = %{
"user" => %{
"id" => 123,
"name" => "asdf",
"email" => "foo@bar.com",
"updated_at" => "2017-09-12T14:44:55Z"
}
}
conn =
:post
|> Plug.Test.conn("/api/users", Jason.encode!(request_body))
|> Plug.Conn.put_req_header("content-type", "application/json; charset=UTF-8")
|> OpenApiSpexTest.Router.call([])
assert conn.body_params == %OpenApiSpexTest.Schemas.UserRequest{
user: %OpenApiSpexTest.Schemas.User{
id: 123,
name: "asdf",
email: "foo@bar.com",
updated_at: ~N[2017-09-12T14:44:55Z] |> DateTime.from_naive!("Etc/UTC")
}
}
assert Jason.decode!(conn.resp_body) == %{
"data" => %{
"email" => "foo@bar.com",
"id" => 1234,
"inserted_at" => nil,
"name" => "asdf",
"updated_at" => "2017-09-12T14:44:55Z"
}
}
end
test "Invalid Request" do
request_body = %{
"user" => %{
"id" => 123,
"name" => "*1234",
"email" => "foo@bar.com",
"updated_at" => "2017-09-12T14:44:55Z"
}
}
conn =
:post
|> Plug.Test.conn("/api/users", Jason.encode!(request_body))
|> Plug.Conn.put_req_header("content-type", "application/json")
conn = OpenApiSpexTest.Router.call(conn, [])
assert conn.status == 422
resp_data = Jason.decode!(conn.resp_body)
assert resp_data ==
%{
"errors" => [
%{
"message" => "Invalid format. Expected ~r/[a-zA-Z][a-zA-Z0-9_]+/",
"source" => %{"pointer" => "/user/name"},
"title" => "Invalid value"
}
]
}
end
end
+
+ describe "oneOf body params" do
+ test "Valid Request" do
+ request_body = %{
+ "pet" => %{
+ "pet_type" => "Dog",
+ "bark" => "woof"
+ }
+ }
+
+ conn =
+ :post
+ |> Plug.Test.conn("/api/pets", Jason.encode!(request_body))
+ |> Plug.Conn.put_req_header("content-type", "application/json; charset=UTF-8")
+ |> OpenApiSpexTest.Router.call([])
+
+ assert conn.body_params == %OpenApiSpexTest.Schemas.PetRequest{
+ pet: %OpenApiSpexTest.Schemas.Dog{
+ pet_type: "Dog",
+ bark: "woof"
+ }
+ }
+
+ assert Jason.decode!(conn.resp_body) == %{
+ "data" => %{
+ "pet_type" => "Dog",
+ "bark" => "woof"
+ }
+ }
+ end
+
+ @tag :capture_log
+ test "Invalid Request" do
+ request_body = %{
+ "pet" => %{
+ "pet_type" => "Human",
+ "says" => "yes"
+ }
+ }
+
+ conn =
+ :post
+ |> Plug.Test.conn("/api/pets", Jason.encode!(request_body))
+ |> Plug.Conn.put_req_header("content-type", "application/json")
+
+ conn = OpenApiSpexTest.Router.call(conn, [])
+ assert conn.status == 422
+
+ resp_body = Jason.decode!(conn.resp_body)
+
+ assert resp_body == %{
+ "errors" => [
+ %{
+ "source" => %{
+ "pointer" => "/pet"
+ },
+ "title" => "Invalid value",
+ "message" => "Failed to cast value to one of: [] (no schemas provided)"
+ }
+ ]
+ }
+ end
+ end
end
diff --git a/test/schema_test.exs b/test/schema_test.exs
index e735eb2..65182b3 100644
--- a/test/schema_test.exs
+++ b/test/schema_test.exs
@@ -1,474 +1,477 @@
defmodule OpenApiSpex.SchemaTest do
use ExUnit.Case
alias OpenApiSpex.Schema
alias OpenApiSpexTest.{ApiSpec, Schemas}
import OpenApiSpex.TestAssertions
doctest Schema
describe "schema/1" do
test "EntityWithDict Schema example matches schema" do
api_spec = ApiSpec.spec()
assert_schema(Schemas.EntityWithDict.schema().example, "EntityWithDict", api_spec)
end
test "User Schema example matches schema" do
spec = ApiSpec.spec()
assert_schema(Schemas.User.schema().example, "User", spec)
assert_schema(Schemas.UserRequest.schema().example, "UserRequest", spec)
assert_schema(Schemas.UserResponse.schema().example, "UserResponse", spec)
assert_schema(Schemas.UsersResponse.schema().example, "UsersResponse", spec)
end
end
describe "cast/3" do
test "cast request schema" do
api_spec = ApiSpec.spec()
schemas = api_spec.components.schemas
user_request_schema = schemas["UserRequest"]
input = %{
"user" => %{
"id" => 123,
"name" => "asdf",
"email" => "foo@bar.com",
"updated_at" => "2017-09-12T14:44:55Z"
}
}
{:ok, output} = Schema.cast(user_request_schema, input, schemas)
assert output == %OpenApiSpexTest.Schemas.UserRequest{
user: %OpenApiSpexTest.Schemas.User{
id: 123,
name: "asdf",
email: "foo@bar.com",
updated_at: DateTime.from_naive!(~N[2017-09-12T14:44:55], "Etc/UTC")
}
}
end
test "cast request schema with unexpected fields returns error" do
api_spec = ApiSpec.spec()
schemas = api_spec.components.schemas
user_request_schema = schemas["UserRequest"]
input = %{
"user" => %{
"id" => 123,
"name" => "asdf",
"email" => "foo@bar.com",
"updated_at" => "2017-09-12T14:44:55Z",
"unexpected_field" => "unexpected value"
}
}
assert {:error, _} = Schema.cast(user_request_schema, input, schemas)
end
test "Cast Cat from Pet schema" do
api_spec = ApiSpec.spec()
schemas = api_spec.components.schemas
pet_schema = schemas["Pet"]
input = %{
"pet_type" => "Cat",
"meow" => "meow"
}
assert {:ok, %Schemas.Cat{meow: "meow", pet_type: "Cat"}} =
Schema.cast(pet_schema, input, schemas)
end
test "Cast Dog from oneOf [cat, dog] schema" do
api_spec = ApiSpec.spec()
schemas = api_spec.components.schemas
cat_or_dog = Map.fetch!(schemas, "CatOrDog")
input = %{
"pet_type" => "Dog",
"bark" => "bowow"
}
assert {:ok, %Schemas.Dog{bark: "bowow", pet_type: "Dog"}} =
Schema.cast(cat_or_dog, input, schemas)
end
test "Cast Cat from oneOf [cat, dog] schema" do
api_spec = ApiSpec.spec()
schemas = api_spec.components.schemas
cat_or_dog = Map.fetch!(schemas, "CatOrDog")
input = %{
"pet_type" => "Cat",
"meow" => "meow"
}
assert {:ok, %Schemas.Cat{meow: "meow", pet_type: "Cat"}} =
Schema.cast(cat_or_dog, input, schemas)
end
test "Cast number to string or number" do
schema = %Schema{
oneOf: [
%Schema{type: :number},
%Schema{type: :string}
]
}
+ # The following object is valid against both schemas, so it will result in an error
+ # – it should be valid against only one of the schemas, since we are using the oneOf keyword.
+ # @see https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/#oneof
result = Schema.cast(schema, "123", %{})
- assert {:ok, 123.0} = result
+ assert {:error, _} = result
end
test "Cast integer to float" do
schema = %Schema{type: :number}
assert Schema.cast(schema, 123, %{}) === {:ok, 123.0}
end
test "Cast string to oneOf number or date-time" do
schema = %Schema{
oneOf: [
%Schema{type: :number},
%Schema{type: :string, format: :"date-time"}
]
}
assert {:ok, %DateTime{}} = Schema.cast(schema, "2018-04-01T12:34:56Z", %{})
end
test "Cast string to anyOf number or date-time" do
schema = %Schema{
oneOf: [
%Schema{type: :number},
%Schema{type: :string, format: :"date-time"}
]
}
assert {:ok, %DateTime{}} = Schema.cast(schema, "2018-04-01T12:34:56Z", %{})
end
end
describe "Integer validation" do
test "Validate schema type integer when value is object" do
schema = %Schema{
type: :integer
}
assert {:error, _} = Schema.validate(schema, %{}, %{})
end
end
describe "Number validation" do
test "Validate schema type number when value is object" do
schema = %Schema{
type: :integer
}
assert {:error, _} = Schema.validate(schema, %{}, %{})
end
test "Validate schema type number when value is exclusive" do
schema = %Schema{
type: :integer,
minimum: -1,
maximum: 1,
exclusiveMinimum: true,
exclusiveMaximum: true
}
assert {:error, _} = Schema.validate(schema, 1, %{})
assert {:error, _} = Schema.validate(schema, -1, %{})
end
end
describe "String validation" do
test "Validate schema type string when value is object" do
schema = %Schema{
type: :string
}
assert {:error, _} = Schema.validate(schema, %{}, %{})
end
test "Validate schema type string when value is DateTime" do
schema = %Schema{
type: :string
}
assert {:error, _} = Schema.validate(schema, DateTime.utc_now(), %{})
end
test "Validate non-empty string with expected value" do
schema = %Schema{type: :string, minLength: 1}
assert :ok = Schema.validate(schema, "BLIP", %{})
end
end
describe "DateTime validation" do
test "Validate schema type string with format date-time when value is DateTime" do
schema = %Schema{
type: :string,
format: :"date-time"
}
assert :ok = Schema.validate(schema, DateTime.utc_now(), %{})
end
end
describe "Date Validation" do
test "Validate schema type string with format date when value is Date" do
schema = %Schema{
type: :string,
format: :date
}
assert :ok = Schema.validate(schema, Date.utc_today(), %{})
end
end
describe "Enum validation" do
test "Validate string enum with unexpected value" do
schema = %Schema{
type: :string,
enum: ["foo", "bar"]
}
assert {:error, _} = Schema.validate(schema, "baz", %{})
end
test "Validate string enum with expected value" do
schema = %Schema{
type: :string,
enum: ["foo", "bar"]
}
assert :ok = Schema.validate(schema, "bar", %{})
end
end
describe "Object validation" do
test "Validate schema type object when value is array" do
schema = %Schema{
type: :object
}
assert {:error, _} = Schema.validate(schema, [], %{})
end
test "Validate schema type object when value is DateTime" do
schema = %Schema{
type: :object
}
assert {:error, _} = Schema.validate(schema, DateTime.utc_now(), %{})
end
end
describe "Array validation" do
test "Validate schema type array when value is object" do
schema = %Schema{
type: :array
}
assert {:error, _} = Schema.validate(schema, %{}, %{})
end
end
describe "Boolean validation" do
test "Validate schema type boolean when value is object" do
schema = %Schema{
type: :boolean
}
assert {:error, _} = Schema.validate(schema, %{}, %{})
end
end
describe "AnyOf validation" do
test "Validate anyOf schema with valid value" do
schema = %Schema{
anyOf: [
%Schema{type: :array},
%Schema{type: :string}
]
}
assert :ok = Schema.validate(schema, "a string", %{})
end
test "Validate anyOf with value matching more than one schema" do
schema = %Schema{
anyOf: [
%Schema{type: :number},
%Schema{type: :integer}
]
}
assert :ok = Schema.validate(schema, 42, %{})
end
test "Validate anyOf schema with invalid value" do
schema = %Schema{
anyOf: [
%Schema{type: :string},
%Schema{type: :array}
]
}
assert {:error, _} = Schema.validate(schema, 3.14159, %{})
end
end
describe "OneOf validation" do
test "Validate oneOf schema with valid value" do
schema = %Schema{
oneOf: [
%Schema{type: :string},
%Schema{type: :array}
]
}
assert :ok = Schema.validate(schema, [1, 2, 3], %{})
end
test "Validate oneOf schema with invalid value" do
schema = %Schema{
oneOf: [
%Schema{type: :string},
%Schema{type: :array}
]
}
assert {:error, _} = Schema.validate(schema, 3.14159, %{})
end
test "Validate oneOf schema when matching multiple schemas" do
schema = %Schema{
oneOf: [
%Schema{type: :object, properties: %{a: %Schema{type: :string}}},
%Schema{type: :object, properties: %{b: %Schema{type: :string}}}
]
}
assert {:error, _} = Schema.validate(schema, %{a: "a", b: "b"}, %{})
end
end
describe "AllOf validation" do
test "Validate allOf schema with valid value" do
schema = %Schema{
allOf: [
%Schema{type: :object, properties: %{a: %Schema{type: :string}}},
%Schema{type: :object, properties: %{b: %Schema{type: :string}}}
]
}
assert :ok = Schema.validate(schema, %{a: "a", b: "b"}, %{})
end
test "Validate allOf schema with invalid value" do
schema = %Schema{
allOf: [
%Schema{type: :object, properties: %{a: %Schema{type: :string}}},
%Schema{type: :object, properties: %{b: %Schema{type: :string}}}
]
}
assert {:error, msg} = Schema.validate(schema, %{a: 1, b: 2}, %{})
assert msg =~ "#/a"
assert msg =~ "#/b"
end
test "Validate allOf with value matching not all schemas" do
schema = %Schema{
allOf: [
%Schema{
type: :integer,
minimum: 5
},
%Schema{
type: :integer,
maximum: 40
}
]
}
assert {:error, _} = Schema.validate(schema, 42, %{})
end
end
describe "Not validation" do
test "Validate not schema with valid value" do
schema = %Schema{
not: %Schema{type: :object}
}
assert :ok = Schema.validate(schema, 1, %{})
end
test "Validate not schema with invalid value" do
schema = %Schema{
not: %Schema{type: :object}
}
assert {:error, _} = Schema.validate(schema, %{a: 1}, %{})
end
test "Verify 'not' validation" do
schema = %Schema{not: %Schema{type: :boolean}}
assert :ok = Schema.validate(schema, 42, %{})
assert :ok = Schema.validate(schema, "42", %{})
assert :ok = Schema.validate(schema, nil, %{})
assert :ok = Schema.validate(schema, 4.2, %{})
assert :ok = Schema.validate(schema, [4], %{})
assert :ok = Schema.validate(schema, %{}, %{})
assert {:error, _} = Schema.validate(schema, true, %{})
assert {:error, _} = Schema.validate(schema, false, %{})
end
end
describe "Nullable validation" do
test "Validate nullable-ified with expected value" do
schema = %Schema{
nullable: true,
type: :string,
minLength: 1
}
assert :ok = Schema.validate(schema, "BLIP", %{})
end
test "Validate nullable with expected value" do
schema = %Schema{type: :string, nullable: true}
assert :ok = Schema.validate(schema, nil, %{})
end
test "Validate nullable with unexpected value" do
schema = %Schema{type: :string, nullable: true}
assert :ok = Schema.validate(schema, "bla", %{})
end
end
describe "Default property value" do
test "Available in structure" do
size = %Schemas.Size{}
assert "cm" == size.unit
assert 100 == size.value
end
test "Available after cast" do
api_spec = ApiSpec.spec()
schemas = api_spec.components.schemas
size = Map.fetch!(schemas, "Size")
assert {:ok, %Schemas.Size{value: 100, unit: "cm"}} ==
Schema.cast(size, %{}, schemas)
assert {:ok, %Schemas.Size{value: 110, unit: "cm"}} ==
Schema.cast(size, %{value: 110}, schemas)
end
end
end
diff --git a/test/support/api_spec.ex b/test/support/api_spec.ex
index 162ebcf..d2b36af 100644
--- a/test/support/api_spec.ex
+++ b/test/support/api_spec.ex
@@ -1,43 +1,44 @@
defmodule OpenApiSpexTest.ApiSpec do
alias OpenApiSpex.{OpenApi, Contact, License, Paths, Server, Info, Components}
alias OpenApiSpexTest.{Router, Schemas}
@behaviour OpenApi
@impl OpenApi
def spec() do
%OpenApi{
servers: [
%Server{url: "http://example.com"}
],
info: %Info{
title: "A",
version: "3.0",
contact: %Contact{
name: "joe",
email: "Joe@gmail.com",
url: "https://help.joe.com"
},
license: %License{
name: "MIT",
url: "http://mit.edu/license"
}
},
components: %Components{
schemas:
for schemaMod <- [
Schemas.Pet,
+ Schemas.PetType,
Schemas.Cat,
Schemas.Dog,
Schemas.CatOrDog,
Schemas.Size
],
into: %{} do
schema = schemaMod.schema()
{schema.title, schema}
end
},
paths: Paths.from_router(Router)
}
|> OpenApiSpex.resolve_schema_modules()
end
end
diff --git a/test/support/pet_controller.ex b/test/support/pet_controller.ex
new file mode 100644
index 0000000..2e01a52
--- /dev/null
+++ b/test/support/pet_controller.ex
@@ -0,0 +1,90 @@
+defmodule OpenApiSpexTest.PetController do
+ use Phoenix.Controller
+ alias OpenApiSpex.Operation
+ alias OpenApiSpexTest.Schemas
+
+ plug OpenApiSpex.Plug.CastAndValidate
+
+ def open_api_operation(action) do
+ apply(__MODULE__, :"#{action}_operation", [])
+ end
+
+ @doc """
+ API Spec for :show action
+ """
+ def show_operation() do
+ import Operation
+
+ %Operation{
+ tags: ["pets"],
+ summary: "Show pet",
+ description: "Show a pet by ID",
+ operationId: "PetController.show",
+ parameters: [
+ parameter(:id, :path, :integer, "Pet ID", example: 123, minimum: 1)
+ ],
+ responses: %{
+ 200 => response("Pet", "application/json", Schemas.PetResponse)
+ }
+ }
+ end
+
+ def show(conn, %{id: id}) do
+ json(conn, %Schemas.PetResponse{
+ data: %Schemas.Dog{
+ pet_type: "Dog",
+ bark: "woof"
+ }
+ })
+ end
+
+ def index_operation() do
+ import Operation
+
+ %Operation{
+ tags: ["pets"],
+ summary: "List pets",
+ description: "List all petes",
+ operationId: "PetController.index",
+ parameters: [
+ parameter(:validParam, :query, :boolean, "Valid Param", example: true)
+ ],
+ responses: %{
+ 200 => response("Pet List Response", "application/json", Schemas.PetsResponse)
+ }
+ }
+ end
+
+ def index(conn, _params) do
+ json(conn, %Schemas.PetsResponse{
+ data: [
+ %Schemas.Dog{
+ pet_type: "Dog",
+ bark: "joe@gmail.com"
+ }
+ ]
+ })
+ end
+
+ def create_operation() do
+ import Operation
+
+ %Operation{
+ tags: ["pets"],
+ summary: "Create pet",
+ description: "Create a pet",
+ operationId: "PetController.create",
+ parameters: [],
+ requestBody: request_body("The pet attributes", "application/json", Schemas.PetRequest),
+ responses: %{
+ 201 => response("Pet", "application/json", Schemas.PetRequest)
+ }
+ }
+ end
+
+ def create(conn = %{body_params: %Schemas.PetRequest{pet: pet}}, _) do
+ json(conn, %Schemas.PetResponse{
+ data: pet
+ })
+ end
+end
diff --git a/test/support/router.ex b/test/support/router.ex
index 97b6a21..603838e 100644
--- a/test/support/router.ex
+++ b/test/support/router.ex
@@ -1,24 +1,26 @@
defmodule OpenApiSpexTest.Router do
use Phoenix.Router
alias Plug.Parsers
alias OpenApiSpex.Plug.{PutApiSpec, RenderSpec}
pipeline :api do
plug :accepts, ["json"]
plug PutApiSpec, module: OpenApiSpexTest.ApiSpec
plug Parsers, parsers: [:json], pass: ["text/*"], json_decoder: Jason
end
scope "/api", OpenApiSpexTest do
pipe_through :api
resources "/users", UserController, only: [:create, :index, :show]
# Used by ParamsTest
resources "/custom_error_users", CustomErrorUserController, only: [:index]
get "/users/:id/payment_details", UserController, :payment_details
post "/users/:id/contact_info", UserController, :contact_info
post "/users/create_entity", UserController, :create_entity
get "/openapi", RenderSpec, []
+
+ resources "/pets", PetController, only: [:create, :index, :show]
end
end
diff --git a/test/support/schemas.ex b/test/support/schemas.ex
index 183ef16..f571e0f 100644
--- a/test/support/schemas.ex
+++ b/test/support/schemas.ex
@@ -1,298 +1,388 @@
defmodule OpenApiSpexTest.Schemas do
require OpenApiSpex
+ alias OpenApiSpex.Reference
alias OpenApiSpex.Schema
+ defmodule Helper do
+ def prepare_struct([%Reference{"$ref": "#/components/schemas/" <> name} | tail]) do
+ schema =
+ apply(String.to_existing_atom("Elixir.OpenApiSpexTest.Schemas." <> name), :schema, [])
+
+ prepare_struct([schema | tail])
+ end
+
+ def prepare_struct([%Schema{properties: props} | tail]) when is_map(props) do
+ keys = Map.keys(props)
+ keys ++ prepare_struct(tail)
+ end
+
+ def prepare_struct([%Schema{allOf: allOf} | tail]) when is_list(allOf) do
+ prepare_struct(allOf ++ tail)
+ end
+
+ def prepare_struct([module_name | tail]) when is_atom(module_name) do
+ prepare_struct([module_name.schema() | tail])
+ end
+
+ def prepare_struct([]) do
+ []
+ end
+ end
+
defmodule Size do
OpenApiSpex.schema(%{
title: "Size",
description: "A size of a pet",
type: :object,
properties: %{
unit: %Schema{type: :string, description: "SI unit name", default: "cm"},
value: %Schema{type: :integer, description: "Size in given unit", default: 100}
},
required: [:unit, :value]
})
end
defmodule User do
OpenApiSpex.schema(%{
title: "User",
description: "A user of the app",
type: :object,
properties: %{
id: %Schema{type: :integer, description: "User ID"},
name: %Schema{type: :string, description: "User name", pattern: ~r/[a-zA-Z][a-zA-Z0-9_]+/},
email: %Schema{type: :string, description: "Email address", format: :email},
inserted_at: %Schema{
type: :string,
description: "Creation timestamp",
format: :"date-time"
},
updated_at: %Schema{type: :string, description: "Update timestamp", format: :"date-time"}
},
required: [:name, :email],
additionalProperties: false,
example: %{
"id" => 123,
"name" => "Joe User",
"email" => "joe@gmail.com",
"inserted_at" => "2017-09-12T12:34:55Z",
"updated_at" => "2017-09-13T10:11:12Z"
}
})
end
defmodule ContactInfo do
OpenApiSpex.schema(%{
title: "ContactInfo",
description: "A users contact information",
type: :object,
properties: %{
phone_number: %Schema{type: :string, description: "Phone number"},
postal_address: %Schema{type: :string, description: "Postal address"}
},
required: [:phone_number],
additionalProperties: false,
example: %{
"phone_number" => "555-123-456",
"postal_address" => "123 Evergreen Tce"
}
})
end
defmodule CreditCardPaymentDetails do
OpenApiSpex.schema(%{
title: "CreditCardPaymentDetails",
description: "Payment details when using credit-card method",
type: :object,
properties: %{
credit_card_number: %Schema{type: :string, description: "Credit card number"},
name_on_card: %Schema{type: :string, description: "Name as appears on card"},
expiry: %Schema{type: :string, description: "4 digit expiry MMYY"}
},
required: [:credit_card_number, :name_on_card, :expiry],
example: %{
"credit_card_number" => "1234-5678-1234-6789",
"name_on_card" => "Joe User",
"expiry" => "1234"
}
})
end
defmodule DirectDebitPaymentDetails do
OpenApiSpex.schema(%{
title: "DirectDebitPaymentDetails",
description: "Payment details when using direct-debit method",
type: :object,
properties: %{
account_number: %Schema{type: :string, description: "Bank account number"},
account_name: %Schema{type: :string, description: "Name of account"},
bsb: %Schema{type: :string, description: "Branch identifier"}
},
required: [:account_number, :account_name, :bsb],
example: %{
"account_number" => "12349876",
"account_name" => "Joes Savings Account",
"bsb" => "123-4567"
}
})
end
defmodule PaymentDetails do
OpenApiSpex.schema(%{
title: "PaymentDetails",
description: "Abstract Payment details type",
type: :object,
oneOf: [
CreditCardPaymentDetails,
DirectDebitPaymentDetails
]
})
end
defmodule UserRequest do
OpenApiSpex.schema(%{
title: "UserRequest",
description: "POST body for creating a user",
type: :object,
properties: %{
user: User
},
example: %{
"user" => %{
"name" => "Joe User",
"email" => "joe@gmail.com"
}
}
})
end
defmodule UserResponse do
OpenApiSpex.schema(%{
title: "UserResponse",
description: "Response schema for single user",
type: :object,
properties: %{
data: User
},
example: %{
"data" => %{
"id" => 123,
"name" => "Joe User",
"email" => "joe@gmail.com",
"inserted_at" => "2017-09-12T12:34:55Z",
"updated_at" => "2017-09-13T10:11:12Z"
}
}
})
end
defmodule UsersResponse do
OpenApiSpex.schema(%{
title: "UsersResponse",
description: "Response schema for multiple users",
type: :object,
properties: %{
data: %Schema{description: "The users details", type: :array, items: User}
},
example: %{
"data" => [
%{
"id" => 123,
"name" => "Joe User",
"email" => "joe@gmail.com"
},
%{
"id" => 456,
"name" => "Jay Consumer",
"email" => "jay@yahoo.com"
}
]
}
})
end
defmodule EntityWithDict do
OpenApiSpex.schema(%{
title: "EntityWithDict",
description: "Entity with a dictionary defined via additionalProperties",
type: :object,
properties: %{
id: %Schema{type: :integer, description: "Entity ID"},
stringDict: %Schema{
type: :object,
description: "String valued dict",
additionalProperties: %Schema{type: :string}
},
anyTypeDict: %Schema{
type: :object,
description: "Untyped valued dict",
additionalProperties: true
}
},
example: %{
"id" => 123,
"stringDict" => %{"key1" => "value1", "key2" => "value2"},
"anyTypeDict" => %{"key1" => 42, "key2" => %{"foo" => "bar"}}
}
})
end
- defmodule Pet do
+ defmodule PetType do
require OpenApiSpex
- alias OpenApiSpex.{Schema, Discriminator}
-
- pet_schemas = [
- %Schema{
- title: "Dog",
- type: :object,
- properties: %{
- bark: %Schema{type: :string}
- }
- },
- %Schema{
- title: "Cat",
- properties: %{
- meow: %Schema{type: :string}
- }
- }
- ]
-
OpenApiSpex.schema(%{
- title: "Pet",
+ title: "PetType",
type: :object,
- properties: %{
- pet_type: %Schema{type: :string}
- },
required: [:pet_type],
- anyOf: pet_schemas,
- discriminator: %Discriminator{
- propertyName: "pet_type"
+ properties: %{
+ pet_type: %Schema{
+ type: :string
+ }
}
})
end
defmodule Cat do
- require OpenApiSpex
-
alias OpenApiSpex.Schema
- OpenApiSpex.schema(%{
+ @behaviour OpenApiSpex.Schema
+ @derive [Jason.Encoder]
+ @schema %Schema{
title: "Cat",
type: :object,
allOf: [
- Pet,
+ PetType,
%Schema{
type: :object,
properties: %{
meow: %Schema{type: :string}
},
required: [:meow]
}
- ]
- })
+ ],
+ "x-struct": __MODULE__
+ }
+
+ def schema, do: @schema
+ defstruct OpenApiSpexTest.Schemas.Helper.prepare_struct(@schema.allOf)
end
defmodule Dog do
- require OpenApiSpex
-
alias OpenApiSpex.Schema
- OpenApiSpex.schema(%{
+ @behaviour OpenApiSpex.Schema
+ @derive [Jason.Encoder]
+ @schema %Schema{
title: "Dog",
type: :object,
allOf: [
- Pet,
+ PetType,
%Schema{
type: :object,
properties: %{
bark: %Schema{type: :string}
},
required: [:bark]
}
- ]
- })
+ ],
+ "x-struct": __MODULE__
+ }
+
+ def schema, do: @schema
+ defstruct OpenApiSpexTest.Schemas.Helper.prepare_struct(@schema.allOf)
end
defmodule CatOrDog do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "CatOrDog",
oneOf: [Cat, Dog]
})
end
defmodule PrimitiveArray do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "PrimitiveArray",
type: :array,
items: %Schema{type: "string"},
example: ["Foo"]
})
end
+
+ defmodule PetResponse do
+ OpenApiSpex.schema(%{
+ title: "PetResponse",
+ description: "Response schema for single pet",
+ type: :object,
+ properties: %{
+ data: OpenApiSpexTest.Schemas.CatOrDog
+ },
+ example: %{
+ "data" => %{
+ "pet_type" => "Dog",
+ "bark" => "woof"
+ }
+ }
+ })
+ end
+
+ defmodule Pet do
+ require OpenApiSpex
+ alias OpenApiSpex.{Schema, Discriminator}
+
+ OpenApiSpex.schema(%{
+ title: "Pet",
+ type: :object,
+ oneOf: [Cat, Dog],
+ discriminator: %Discriminator{
+ propertyName: "pet_type"
+ }
+ })
+ end
+
+ defmodule PetsResponse do
+ OpenApiSpex.schema(%{
+ title: "PetsResponse",
+ description: "Response schema for multiple pets",
+ type: :object,
+ properties: %{
+ data: %Schema{
+ description: "The pets details",
+ type: :array,
+ items: OpenApiSpexTest.Schemas.CatOrDog
+ }
+ },
+ example: %{
+ "data" => [
+ %{
+ "pet_type" => "Dog",
+ "bark" => "woof"
+ },
+ %{
+ "pet_type" => "Cat",
+ "meow" => "meow"
+ }
+ ]
+ }
+ })
+ end
+
+ defmodule PetRequest do
+ OpenApiSpex.schema(%{
+ title: "PetRequest",
+ description: "POST body for creating a pet",
+ type: :object,
+ properties: %{
+ pet: CatOrDog
+ },
+ example: %{
+ "pet" => %{
+ "pet_type" => "Dog",
+ "bark" => "woof"
+ }
+ }
+ })
+ end
end

File Metadata

Mime Type
text/x-diff
Expires
Wed, Nov 27, 4:44 PM (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40694
Default Alt Text
(89 KB)

Event Timeline