Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F111963
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
20 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/open_api_spex/reference.ex b/lib/open_api_spex/reference.ex
index 6359bf1..9441dcb 100644
--- a/lib/open_api_spex/reference.ex
+++ b/lib/open_api_spex/reference.ex
@@ -1,8 +1,34 @@
defmodule OpenApiSpex.Reference do
+ @moduledoc """
+ A simple object to allow referencing other components in the specification, internally and externally.
+
+ The Reference Object is defined by JSON Reference and follows the same structure, behavior and rules.
+ See https://swagger.io/specification/#referenceObject
+
+ ## Example
+
+ ref = %OpenApiSpex.Reference{"$ref": "#/components/schemas/user"}
+ """
+
+ alias OpenApiSpex.Reference
+
defstruct [
:"$ref"
]
@type t :: %{
"$ref": String.t
}
+
+ @doc """
+ Resolve a `Reference` to the `Schema` it refers to.
+
+ ## Examples
+
+ iex> alias OpenApiSpex.{Reference, Schema}
+ ...> schemas = %{"user" => %Schema{title: "user", type: :object}}
+ ...> Reference.resolve_schema(%Reference{"$ref": "#/components/schemas/user"}, schemas)
+ %OpenApiSpex.Schema{type: :object, title: "user"}
+ """
+ @spec resolve_schema(Reference.t, %{String.t => Schema.t}) :: Schema.t | nil
+ def resolve_schema(%Reference{"$ref": "#/components/schemas/" <> name}, schemas), do: schemas[name]
end
\ No newline at end of file
diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex
index b22fd38..7bccef6 100644
--- a/lib/open_api_spex/schema.ex
+++ b/lib/open_api_spex/schema.ex
@@ -1,300 +1,372 @@
defmodule OpenApiSpex.Schema do
+ @moduledoc """
+ The Schema Object allows the definition of input and output data types. These types can be objects, but also primitives and arrays.
+
+ See https://swagger.io/specification/#schemaObject
+
+ ## 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"
+ }
+ }
+ """
alias OpenApiSpex.{
Schema, Reference, Discriminator, Xml, ExternalDocumentation
}
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, true},
:description,
:format,
:default,
:nullable,
:discriminator,
:readOnly,
:writeOnly,
:xml,
:externalDocs,
:example,
:deprecated,
:"x-struct"
]
@type t :: %__MODULE__{
title: String.t,
multipleOf: number,
maximum: number,
exclusiveMaximum: boolean,
minimum: number,
exclusiveMinimum: boolean,
maxLength: integer,
minLength: integer,
pattern: String.t,
maxItems: integer,
minItems: integer,
uniqueItems: boolean,
maxProperties: integer,
minProperties: integer,
required: [String.t],
enum: [String.t],
type: String.t,
allOf: [Schema.t | Reference.t],
oneOf: [Schema.t | Reference.t],
anyOf: [Schema.t | Reference.t],
not: Schema.t | Reference.t,
items: Schema.t | Reference.t,
properties: %{String.t => Schema.t | Reference.t},
additionalProperties: boolean | Schema.t | Reference.t,
description: String.t,
format: String.t,
default: any,
nullable: boolean,
discriminator: Discriminator.t,
readOnly: boolean,
writeOnly: boolean,
xml: Xml.t,
externalDocs: ExternalDocumentation.t,
example: any,
deprecated: boolean,
"x-struct": module
}
- def resolve_schema(schema = %Schema{}, _), do: schema
- def resolve_schema(%Reference{"$ref": "#/components/schemas/" <> name}, schemas), do: schemas[name]
+ @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}) :: {: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" -> true
"false" -> 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 = %Schema{type: :object}, value, schemas) when is_map(value) do
with {:ok, props} <- cast_properties(schema, Enum.to_list(value), schemas) do
{:ok, Map.new(props)}
end
end
- def cast(ref = %Reference{}, val, schemas), do: cast(resolve_schema(ref, schemas), val, schemas)
- def cast(additionalProperties, val, _schemas) when is_boolean(additionalProperties), do: val
+ def cast(ref = %Reference{}, val, schemas), do: cast(Reference.resolve_schema(ref, schemas), val, schemas)
+ def cast(additionalProperties, val, _schemas) when additionalProperties in [true, false, nil], do: val
+ @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
- def validate(ref = %Reference{}, val, schemas), do: validate(resolve_schema(ref, schemas), val, schemas)
+
+ @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}) :: :ok | {:error, String.t}
+ def validate(ref = %Reference{}, val, schemas), do: validate(Reference.resolve_schema(ref, schemas), val, schemas)
def validate(schema = %Schema{type: type}, value, _schemas) when type in [:integer, :number] do
with :ok <- validate_multiple(schema, value),
:ok <- validate_maximum(schema, value),
:ok <- validate_minimum(schema, value) do
:ok
end
end
def validate(schema = %Schema{type: :string}, value, _schemas) do
with :ok <- validate_max_length(schema, value),
:ok <- validate_min_length(schema, value),
:ok <- validate_pattern(schema, value) do
:ok
end
end
def validate(%Schema{type: :boolean}, value, _schemas) do
case is_boolean(value) do
true -> :ok
_ -> {:error, "Invalid boolean: #{inspect(value)}"}
end
end
def validate(schema = %Schema{type: :array}, value, schemas) do
with :ok <- validate_max_items(schema, value),
:ok <- validate_min_items(schema, value),
:ok <- validate_unique_items(schema, value),
:ok <- validate_array_items(schema, value, schemas) do
:ok
end
end
def validate(schema = %Schema{type: :object, properties: properties}, value, schemas) do
with :ok <- validate_required_properties(schema, value),
:ok <- validate_max_properties(schema, value),
:ok <- validate_min_properties(schema, value),
:ok <- validate_object_properties(properties, value, schemas) do
:ok
end
end
- def validate_multiple(%{multipleOf: nil}, _), do: :ok
- def validate_multiple(%{multipleOf: n}, value) when (round(value / n) * n == value), do: :ok
- def validate_multiple(%{multipleOf: n}, value), do: {:error, "#{value} is not a multiple of #{n}"}
+ @spec validate_multiple(Schema.t, number) :: :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), do: {:error, "#{value} is not a multiple of #{n}"}
- def validate_maximum(%{maximum: nil}, _), do: :ok
- def validate_maximum(%{maximum: n, exclusiveMaximum: true}, value) when value < n, do: :ok
- def validate_maximum(%{maximum: n}, value) when value <= n, do: :ok
- def validate_maximum(%{maximum: n}, value), do: {:error, "#{value} is larger than maximum #{n}"}
+ @spec validate_maximum(Schema.t, number) :: :ok | {:error, String.t}
+ defp validate_maximum(%{maximum: nil}, _), do: :ok
+ defp validate_maximum(%{maximum: n, exclusiveMaximum: true}, value) when value < n, do: :ok
+ defp validate_maximum(%{maximum: n}, value) when value <= n, do: :ok
+ defp validate_maximum(%{maximum: n}, value), do: {:error, "#{value} is larger than maximum #{n}"}
- def validate_minimum(%{minimum: nil}, _), do: :ok
- def validate_minimum(%{minimum: n, exclusiveMinimum: true}, value) when value > n, do: :ok
- def validate_minimum(%{minimum: n}, value) when value >= n, do: :ok
- def validate_minimum(%{minimum: n}, value), do: {:error, "#{value} is smaller than minimum #{n}"}
+ @spec validate_minimum(Schema.t, number) :: :ok | {:error, String.t}
+ defp validate_minimum(%{minimum: nil}, _), do: :ok
+ defp validate_minimum(%{minimum: n, exclusiveMinimum: true}, value) when value > n, do: :ok
+ defp validate_minimum(%{minimum: n}, value) when value >= n, do: :ok
+ defp validate_minimum(%{minimum: n}, value), do: {:error, "#{value} is smaller than minimum #{n}"}
- def validate_max_length(%{maxLength: nil}, _), do: :ok
- def validate_max_length(%{maxLength: n}, value) do
+ @spec validate_max_length(Schema.t, String.t) :: :ok | {:error, String.t}
+ defp validate_max_length(%{maxLength: nil}, _), do: :ok
+ defp validate_max_length(%{maxLength: n}, value) do
case String.length(value) <= n do
true -> :ok
_ -> {:error, "String length is larger than maxLength: #{n}"}
end
end
- def validate_min_length(%{minLength: nil}, _), do: :ok
- def validate_min_length(%{minLength: n}, value) do
+ @spec validate_min_length(Schema.t, String.t) :: :ok | {:error, String.t}
+ defp validate_min_length(%{minLength: nil}, _), do: :ok
+ defp validate_min_length(%{minLength: n}, value) do
case String.length(value) >= n do
true -> :ok
_ -> {:error, "String length is smaller than minLength: #{n}"}
end
end
- def validate_pattern(%{pattern: nil}, _), do: :ok
- def validate_pattern(schema = %{pattern: regex}, val) when is_binary(regex) do
- validate_pattern(%{schema | pattern: Regex.compile(regex)}, val)
+ @spec validate_max_length(Schema.t, String.t) :: :ok | {:error, String.t}
+ defp validate_pattern(%{pattern: nil}, _), do: :ok
+ defp validate_pattern(schema = %{pattern: regex}, val) when is_binary(regex) do
+ with {:ok, regex} <- Regex.compile(regex) do
+ validate_pattern(%{schema | pattern: regex}, val)
+ end
end
- def validate_pattern(%{pattern: regex = %Regex{}}, val) do
+ defp validate_pattern(%{pattern: regex = %Regex{}}, val) do
case Regex.match?(regex, val) do
true -> :ok
_ -> {:error, "Value does not match pattern: #{regex.source}"}
end
end
- def validate_max_items(%Schema{maxItems: nil}, _), do: :ok
- def validate_max_items(%Schema{maxItems: n}, value) when length(value) <= n, do: :ok
- def validate_max_items(%Schema{maxItems: n}, value) do
+ @spec validate_max_length(Schema.t, list) :: :ok | {:error, String.t}
+ defp validate_max_items(%Schema{maxItems: nil}, _), do: :ok
+ defp validate_max_items(%Schema{maxItems: n}, value) when length(value) <= n, do: :ok
+ defp validate_max_items(%Schema{maxItems: n}, value) do
{:error, "Array length #{length(value)} is larger than maxItems: #{n}"}
end
- def validate_min_items(%Schema{minItems: nil}, _), do: :ok
- def validate_min_items(%Schema{minItems: n}, value) when length(value) >= n, do: :ok
- def validate_min_items(%Schema{minItems: n}, value) do
+ @spec validate_min_items(Schema.t, list) :: :ok | {:error, String.t}
+ defp validate_min_items(%Schema{minItems: nil}, _), do: :ok
+ defp validate_min_items(%Schema{minItems: n}, value) when length(value) >= n, do: :ok
+ defp validate_min_items(%Schema{minItems: n}, value) do
{:error, "Array length #{length(value)} is smaller than minItems: #{n}"}
end
- def validate_unique_items(%Schema{uniqueItems: true}, value) do
+ @spec validate_unique_items(Schema.t, list) :: :ok | {:error, String.t}
+ defp validate_unique_items(%Schema{uniqueItems: true}, value) do
unique_size =
value
|> MapSet.new()
|> MapSet.size()
case unique_size == length(value) do
true -> :ok
_ -> {:error, "Array items must be unique"}
end
end
- def validate_unique_items(_, _), do: :ok
+ defp validate_unique_items(_, _), do: :ok
- def validate_array_items(%Schema{type: :array, items: nil}, value, _schemas) when is_list(value), do: :ok
- def validate_array_items(%Schema{type: :array}, [], _schemas), do: :ok
- def validate_array_items(schema = %Schema{type: :array, items: item_schema}, [x | rest], schemas) do
+ @spec validate_array_items(Schema.t, list, %{String.t => Schema.t}) :: :ok | {:error, String.t}
+ defp validate_array_items(%Schema{type: :array, items: nil}, value, _schemas) when is_list(value), do: :ok
+ defp validate_array_items(%Schema{type: :array}, [], _schemas), do: :ok
+ defp validate_array_items(schema = %Schema{type: :array, items: item_schema}, [x | rest], schemas) do
with :ok <- validate(item_schema, x, schemas) do
validate(schema, rest, schemas)
end
end
- def validate_required_properties(%Schema{type: :object, required: nil}, _), do: :ok
- def validate_required_properties(%Schema{type: :object, required: required}, value) do
+ @spec validate_required_properties(Schema.t, %{}) :: :ok | {:error, String.t}
+ defp validate_required_properties(%Schema{type: :object, required: nil}, _), do: :ok
+ defp validate_required_properties(%Schema{type: :object, required: required}, value) do
missing = required -- Map.keys(value)
case missing do
[] -> :ok
_ -> {:error, "Missing required properties: #{inspect(missing)}"}
end
end
- def validate_max_properties(%Schema{type: :object, maxProperties: nil}, _), do: :ok
- def validate_max_properties(%Schema{type: :object, maxProperties: n}, val) when map_size(val) <= n, do: :ok
- def validate_max_properties(%Schema{type: :object, maxProperties: n}, val) do
+ @spec validate_max_properties(Schema.t, %{}) :: :ok | {:error, String.t}
+ defp validate_max_properties(%Schema{type: :object, maxProperties: nil}, _), do: :ok
+ defp validate_max_properties(%Schema{type: :object, maxProperties: n}, val) when map_size(val) <= n, do: :ok
+ defp validate_max_properties(%Schema{type: :object, maxProperties: n}, val) do
{:error, "Object property count #{map_size(val)} is greater than maxProperties: #{n}"}
end
- def validate_min_properties(%Schema{type: :object, minProperties: nil}, _), do: :ok
- def validate_min_properties(%Schema{type: :object, minProperties: n}, val) when map_size(val) >= n, do: :ok
- def validate_min_properties(%Schema{type: :object, minProperties: n}, val) do
+ @spec validate_min_properties(Schema.t, %{}) :: :ok | {:error, String.t}
+ defp validate_min_properties(%Schema{type: :object, minProperties: nil}, _), do: :ok
+ defp validate_min_properties(%Schema{type: :object, minProperties: n}, val) when map_size(val) >= n, do: :ok
+ defp validate_min_properties(%Schema{type: :object, minProperties: n}, val) do
{:error, "Object property count #{map_size(val)} is less than minProperties: #{n}"}
end
- def validate_object_properties(properties = %{}, value, schemas) do
+ @spec validate_min_properties(Schema.t, %{}) :: :ok | {:error, String.t}
+ defp validate_object_properties(properties = %{}, value, schemas) do
properties
|> Enum.filter(fn {name, _schema} -> Map.has_key?(value, name) end)
|> validate_object_properties(value, schemas)
end
- def validate_object_properties([], _, _), do: :ok
- def validate_object_properties([{name, schema} | rest], value, schemas) do
+ defp validate_object_properties([], _, _), do: :ok
+ defp validate_object_properties([{name, schema} | rest], value, schemas) do
case validate(schema, Map.fetch!(value, name), schemas) do
:ok -> validate_object_properties(rest, value, schemas)
error -> error
end
end
end
\ No newline at end of file
diff --git a/test/schema_test.exs b/test/schema_test.exs
index 0fa5ee9..88828a7 100644
--- a/test/schema_test.exs
+++ b/test/schema_test.exs
@@ -1,40 +1,42 @@
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 "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
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Nov 22, 1:14 PM (9 h, 35 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
38687
Default Alt Text
(20 KB)
Attached To
Mode
R22 open_api_spex
Attached
Detach File
Event Timeline
Log In to Comment