Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85823275
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
35 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/open_api_spex.ex b/lib/open_api_spex.ex
index 54f412c..09d35de 100644
--- a/lib/open_api_spex.ex
+++ b/lib/open_api_spex.ex
@@ -1,99 +1,99 @@
defmodule OpenApiSpex do
@moduledoc """
Provides the entry-points for defining schemas, validating and casting.
"""
alias OpenApiSpex.{OpenApi, Operation, Reference, Schema, SchemaResolver}
@doc """
Adds schemas to the api spec from the modules specified in the Operations.
Eg, if the response schema for an operation is defined with:
responses: %{
200 => Operation.response("User", "application/json", UserResponse)
}
Then the `UserResponse.schema()` function will be called to load the schema, and
a `Reference` to the loaded schema will be used in the operation response.
See `OpenApiSpex.schema` macro for a convenient syntax for defining schema modules.
"""
def resolve_schema_modules(spec = %OpenApi{}) do
SchemaResolver.resolve_schema_modules(spec)
end
@doc """
Cast params to conform to a Schema or Operation spec.
"""
@spec cast(OpenApi.t, Schema.t | Reference.t | Operation.t, any) :: {:ok, any} | {:error, String.t}
def cast(spec = %OpenApi{}, schema = %Schema{}, params) do
Schema.cast(schema, params, spec.components.schemas)
end
def cast(spec = %OpenApi{}, schema = %Reference{}, params) do
Schema.cast(schema, params, spec.components.schemas)
end
def cast(spec = %OpenApi{}, operation = %Operation{}, conn = %Plug.Conn{}, content_type \\ nil) do
Operation.cast(operation, conn, content_type, spec.components.schemas)
end
@doc """
Validate params against a Schema or Operation spec.
"""
def validate(spec = %OpenApi{}, schema = %Schema{}, params) do
Schema.validate(schema, params, spec.components.schemas)
end
def validate(spec = %OpenApi{}, schema = %Reference{}, params) do
Schema.validate(schema, params, spec.components.schemas)
end
def validate(spec = %OpenApi{}, operation = %Operation{}, conn = %Plug.Conn{}, content_type \\ nil) do
Operation.validate(operation, conn, content_type, spec.components.schemas)
end
@doc """
Declares a OpenApiSpex Schema
- defines the schema/0 callback
- ensures the schema is linked to the module by "x-struct" extension property
- defines a struct with keys matching the schema properties
- defines a @type `t` for the struct
- derives a `Poison.Encoder` for the struct
## Example
require OpenApiSpex
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],
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
"""
defmacro schema(body) do
quote do
@behaviour OpenApiSpex.Schema
@schema struct(OpenApiSpex.Schema, Map.put(unquote(body), :"x-struct", __MODULE__))
def schema, do: @schema
@derive [Poison.Encoder]
- defstruct (@schema.properties || %{}) |> Map.keys()
+ defstruct Schema.properties(@schema)
@type t :: %__MODULE__{}
end
end
end
diff --git a/lib/open_api_spex/discriminator.ex b/lib/open_api_spex/discriminator.ex
index 546b49e..9b1f5db 100644
--- a/lib/open_api_spex/discriminator.ex
+++ b/lib/open_api_spex/discriminator.ex
@@ -1,24 +1,64 @@
defmodule OpenApiSpex.Discriminator do
@moduledoc """
Defines the `OpenApiSpex.Discriminator.t` type.
"""
+ alias OpenApiSpex.Schema
+
@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.
"""
@type t :: %__MODULE__{
propertyName: String.t,
mapping: %{String.t => String.t} | nil
}
+
+ @doc """
+ Resolve the schema that should be used to cast/validate a value using a `Discriminator`.
+ """
+ @spec resolve(t, map, %{String.t => Schema.t}) :: {:ok, Schema.t} | {:error, String.t}
+ def resolve(%{propertyName: name, mapping: mapping}, value = %{}, schemas = %{}) do
+ with {:ok, val} <- get_property_value(value, name) do
+ mapped = map_property_value(mapping, val)
+ lookup_schema(schemas, mapped)
+ else
+ {:error, reason} -> {:error, reason}
+ end
+ end
+
+ @spec get_property_value(map, String.t) :: {:ok, any} | {:error, String.t}
+ defp get_property_value(value = %{}, property_name) do
+ case Map.fetch(value, property_name) do
+ {:ok, val} -> {:ok, val}
+ :error -> {:error, "No value for required disciminator property: #{property_name}"}
+ end
+ end
+
+ @spec map_property_value(%{String.t => String.t} | nil, String.t) :: String.t
+ defp map_property_value(nil, val), do: val
+ defp map_property_value(mapping = %{}, val) do
+ Map.get(mapping, val, val)
+ end
+
+ @spec lookup_schema(%{String.t => Schema.t}, String.t) :: {:ok, Schema.t} | {:error, String.t}
+ defp lookup_schema(schemas, "#components/schemas/" <> name) do
+ lookup_schema(schemas, name)
+ end
+ defp lookup_schema(schemas, name) do
+ case Map.fetch(schemas, name) do
+ {:ok, schema} -> {:ok, schema}
+ :error -> {:error, "Unknown schema: #{name}"}
+ end
+ end
end
diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex
index 0a11f95..5536a27 100644
--- a/lib/open_api_spex/schema.ex
+++ b/lib/open_api_spex/schema.ex
@@ -1,407 +1,457 @@
defmodule OpenApiSpex.Schema do
@moduledoc """
Defines the `OpenApiSpex.Schema.t` type and operations for casting and validating against a schema.
"""
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: atom | 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 | 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
}
@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"
"""
- @spec cast(Schema.t | Reference.t, any, %{String.t => Schema.t | Reference.t}) :: {:ok, any} | {:error, String.t}
- def cast(schema = %Schema{"x-struct": mod}, value, schemas) when not is_nil(mod) do
- with {:ok, data} <- cast(%{schema | "x-struct": nil}, value, schemas) do
- {:ok, struct(mod, data)}
- end
- end
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_number(value), do: {:ok, value}
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, datetime = %DateTime{}, _offset} -> {:ok, datetime}
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}, 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) do
+ {:ok, make_struct(result, schema)}
+ 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: []}, value = %{}, schemas) do
+ cast(%{schema | allOf: nil}, value, schemas)
+ end
def cast(schema = %Schema{type: :object}, value, schemas) when is_map(value) do
schema = %{schema | properties: schema.properties || %{}}
- with {:ok, props} <- cast_properties(schema, Enum.to_list(value), schemas) do
- {:ok, Map.new(props)}
+ {regular_properties, others} = Enum.split_with(value, fn {k, _v} -> is_binary(k) end)
+ with {:ok, props} <- cast_properties(schema, regular_properties, schemas) do
+ result = Map.new(others ++ props) |> make_struct(schema)
+ {:ok, result}
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: val
+ defp make_struct(val, %{"x-struct": nil}), do: val
+ defp make_struct(val, %{"x-struct": mod}) do
+ Enum.reduce(val, struct(mod), fn {k, v}, acc ->
+ Map.put(acc, k, v)
+ end)
+ end
+
@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 """
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 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) do
{:error, "#{path}: null value where #{type} expected"}
end
def validate(schema = %Schema{type: type}, value, path, _schemas) when type in [:integer, :number] do
with :ok <- validate_multiple(schema, value, path),
:ok <- validate_maximum(schema, value, path),
:ok <- validate_minimum(schema, value, path) do
:ok
end
end
def validate(schema = %Schema{type: :string}, value, path, _schemas) 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
def validate(%Schema{type: :boolean}, value, path, _schemas) do
case is_boolean(value) do
true -> :ok
_ -> {:error, "#{path}: Invalid boolean: #{inspect(value)}"}
end
end
def validate(schema = %Schema{type: :array}, value, path, schemas) 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
@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}, 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}, 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 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
+ Map.keys(properties) ++ 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: []
end
diff --git a/test/schema_test.exs b/test/schema_test.exs
index a5d02c4..0fa0ec0 100644
--- a/test/schema_test.exs
+++ b/test/schema_test.exs
@@ -1,65 +1,78 @@
defmodule OpenApiSpex.SchemaTest do
use ExUnit.Case
alias OpenApiSpex.Schema
alias OpenApiSpexTest.{ApiSpec, Schemas}
import OpenApiSpex.Test.Assertions
doctest Schema
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 "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
\ No newline at end of file
+
+ 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
+end
diff --git a/test/support/api_spec.ex b/test/support/api_spec.ex
index 6018841..fdb5b87 100644
--- a/test/support/api_spec.ex
+++ b/test/support/api_spec.ex
@@ -1,27 +1,34 @@
defmodule OpenApiSpexTest.ApiSpec do
- alias OpenApiSpex.{OpenApi, Contact, License, Paths, Server, Info}
- alias OpenApiSpexTest.Router
+ alias OpenApiSpex.{OpenApi, Contact, License, Paths, Server, Info, Components}
+ alias OpenApiSpexTest.{Router, Schemas}
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: %{
+ "Pet" => Schemas.Pet.schema(),
+ "Cat" => Schemas.Cat.schema(),
+ "Dog" => Schemas.Dog.schema()
+ }
+ },
paths: Paths.from_router(Router)
}
|> OpenApiSpex.resolve_schema_modules()
end
-end
\ No newline at end of file
+end
diff --git a/test/support/schemas.ex b/test/support/schemas.ex
index f2ba17d..e6d4d7f 100644
--- a/test/support/schemas.ex
+++ b/test/support/schemas.ex
@@ -1,159 +1,219 @@
defmodule OpenApiSpexTest.Schemas do
require OpenApiSpex
alias OpenApiSpex.Schema
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 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"
}
},
"x-struct": __MODULE__
}
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
+ require OpenApiSpex
+
+ alias OpenApiSpex.{Schema, Discriminator}
+
+ OpenApiSpex.schema(%{
+ title: "Pet",
+ type: :object,
+ properties: %{
+ pet_type: %Schema{type: :string}
+ },
+ required: [:pet_type],
+ discriminator: %Discriminator{
+ propertyName: "pet_type"
+ }
+ })
+ end
+
+ defmodule Cat do
+ require OpenApiSpex
+
+ alias OpenApiSpex.Schema
+
+ OpenApiSpex.schema(%{
+ title: "Cat",
+ type: :object,
+ allOf: [
+ Pet,
+ %Schema{
+ type: :object,
+ properties: %{
+ meow: %Schema{type: :string}
+ },
+ required: [:meow]
+ }
+ ]
+ })
+ end
+
+ defmodule Dog do
+ require OpenApiSpex
+
+ alias OpenApiSpex.Schema
+
+ OpenApiSpex.schema(%{
+ title: "Dog",
+ type: :object,
+ allOf: [
+ Pet,
+ %Schema{
+ type: :object,
+ properties: %{
+ bark: %Schema{type: :string}
+ },
+ required: [:bark]
+ }
+ ]
+ })
+ end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Oct 29, 3:36 AM (7 h, 18 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1797487
Default Alt Text
(35 KB)
Attached To
Mode
R22 open_api_spex
Attached
Detach File
Event Timeline