Page MenuHomePhorge

No OneTemporary

Size
26 KB
Referenced Files
None
Subscribers
None
diff --git a/README.md b/README.md
index 77bfb87..da252b7 100644
--- a/README.md
+++ b/README.md
@@ -1,273 +1,274 @@
# Open API Spex [![Build Status](https://travis-ci.org/mbuhot/open_api_spex.svg?branch=master)](https://travis-ci.org/mbuhot/open_api_spex)
Add Open API Specification 3 (formerly swagger) to Plug applications.
## Installation
The package can be installed by adding `open_api_spex` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:open_api_spex, github: "mbuhot/open_api_spex"}
]
end
```
## Generating an API spec
Start by adding an `ApiSpec` module to your application.
```elixir
defmodule MyApp.ApiSpec do
alias OpenApiSpex.{OpenApi, Server, Info, Paths}
def spec do
%OpenApi{
servers: [
# Populate the Server info from a phoenix endpoint
Server.from_endpoint(MyAppWeb.Endpoint, otp_app: :my_app)
],
info: %Info{
title: "My App",
version: "1.0"
},
# populate the paths from a phoenix router
paths: Paths.from_router(MyAppWeb.Router)
}
|> OpenApiSpex.resolve_schema_modules() # discover request/response schemas from path specs
end
end
```
For each plug (controller) that will handle api requests, add an `open_api_operation` callback.
It will be passed the plug opts that were declared in the router, this will be the action for a phoenix controller.
```elixir
defmodule MyApp.UserController do
alias OpenApiSpex.Operation
alias MyApp.Schemas.UserResponse
@spec open_api_operation(any) :: Operation.t
def open_api_operation(action), do: apply(__MODULE__, :"#{action}_operation", [])
@spec show_operation() :: Operation.t
def show_operation() do
%Operation{
tags: ["users"],
summary: "Show user",
description: "Show a user by ID",
operationId: "UserController.show",
parameters: [
Operation.parameter(:id, :path, :integer, "User ID", example: 123)
],
responses: %{
200 => Operation.response("User", "application/json", UserResponse)
}
}
end
def show(conn, %{"id" => id}) do
{:ok, user} = MyApp.Users.find_by_id(id)
json(conn, 200, user)
end
end
```
Declare the JSON schemas for request/response bodies in a `Schemas` module:
Each module should export a `schema/0` function, and may optionally declare a struct,
linked to the JSON schema through the `x-struct` extension property.
```elixir
defmodule MyApp.Schemas do
alias OpenApiSpex.Schema
defmodule User do
+ @behaviour OpenApiSpex.Schema
@derive [Poison.Encoder]
@schema %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"},
email: %Schema{type: :string, description: "Email address", format: :email},
inserted_at: %Schema{type: :string, description: "Creation timestamp", format: :datetime},
updated_at: %Schema{type: :string, description: "Update timestamp", format: :datetime}
},
required: [:name, :email],
example: {
"id" => 123,
"name" => "Joe",
"email" => "joe@gmail.com"
}
"x-struct": __MODULE__
}
def schema, do: @schema
defstruct Map.keys(@schema.properties)
end
defmodule UserResponse do
require OpenApiSpex
# OpenApiSpex.schema/1 macro can be optionally used to reduce boilerplate code
OpenApiSpex.schema %{
title: "UserResponse",
description: "Response schema for single user",
type: :object,
properties: %{
data: User
}
}
end
end
```
Now you can create a mix task to write the swagger file to disk:
```elixir
defmodule Mix.Tasks.MyApp.OpenApiSpec do
def run([output_file]) do
json =
MyApp.ApiSpec.spec()
|> Poison.encode!(pretty: true)
:ok = File.write!(output_file, json)
end
end
```
Generate the file with: `mix myapp.openapispec spec.json`
## Serving the API Spec from a Controller
To serve the API spec from your application, first add the `OpenApiSpex.Plug.PutApiSpec` plug somewhere in the pipeline.
```elixir
pipeline :api do
plug OpenApiSpex.Plug.PutApiSpec, module: MyApp.ApiSpec
end
```
Now the spec will be available for use in downstream plugs.
The `OpenApiSpex.Plug.RenderSpec` plug will render the spec as JSON:
```elixir
scope "/api" do
pipe_through :api
resources "/users", MyApp.UserController, only: [:create, :index, :show]
get "/openapi", OpenApiSpex.Plug.RenderSpec, []
end
```
## Swagger UI
Once your API spec is available through a route, the `OpenApiSpex.Plug.SwaggerUI` plug can be used to serve a SwaggerUI interface. The `path:` plug option must be supplied to give the path to the API spec.
All javascript and CSS assets are sourced from cdnjs.cloudflare.com, rather than vendoring into this package.
```elixir
scope "/" do
pipe_through :browser # Use the default browser stack
get "/", MyAppWeb.PageController, :index
get "/swaggerui", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi"
end
scope "/api" do
pipe_through :api
resources "/users", MyAppWeb.UserController, only: [:create, :index, :show]
get "/openapi", OpenApiSpex.Plug.RenderSpec, []
end
```
## Use the API Spec to cast params
Add the `OpenApiSpex.Plug.Cast` plug to a controller to cast the request parameters to elixir types defined by the operation schema.
```elixir
plug OpenApiSpex.Plug.Cast, operation_id: "UserController.show"
```
The `operation_id` can be inferred when used from a Phoenix controller from the contents of `conn.private`.
```elixir
defmodule MyApp.UserController do
use MyAppWeb, :controller
alias OpenApiSpex.Operation
alias MyApp.Schemas.{User, UserRequest, UserResponse}
plug OpenApiSpex.Plug.Cast
def open_api_operation(action) do
apply(__MODULE__, "#{action}_operation", [])
end
def create_operation do
import Operation
%Operation{
tags: ["users"],
summary: "Create user",
description: "Create a user",
operationId: "UserController.create",
parameters: [],
requestBody: request_body("The user attributes", "application/json", UserRequest),
responses: %{
201 => response("User", "application/json", UserResponse)
}
}
end
def create(conn, %UserRequest{user: %User{name: name, email: email, birthday: birthday = %Date{}}}) do
# params are cast to UserRequest struct
end
end
```
## Use the API Spec to validate Requests
Add both the `Cast` and `Validate` plugs to your controller / plug:
```elixir
plug OpenApiSpex.Plug.Cast
plug OpenApiSpex.Plug.Validate
```
Now the client will receive a 422 response whenever the request fails to meet the validation rules from the api spec.
## Validating Schema Examples
As schemas evolve, you may want to confirm that the examples given match the schemas.
Use the `OpenApiSpex.Test.Assertions` module to assert on schema validations.
```elixir
Use ExUnit.Case
import OpenApiSpex.Test.Assertions
test "UsersResponse example matches schema" do
api_spec = MyApp.ApiSpec.spec()
schema = MyApp.Schemas.UsersResponse.schema()
assert_schema(schema.example, "UsersResponse", api_spec)
end
```
## Validating API responses in Tests
Api responses can be tested against schemas using `OpenApiSpex.Test.Assertions` also:
```elixir
use MyApp.ConnCase
import OpenApiSpex.Test.Assertions
test "UserController produces a UsersResponse", %{conn: conn} do
api_spec = MyApp.ApiSpec.spec()
json =
conn
|> get(user_path(conn, :index))
|> json_response(200)
assert_schema(json, "UsersResponse", api_spec)
end
```
diff --git a/lib/open_api_spex.ex b/lib/open_api_spex.ex
index b6d4b64..f846279 100644
--- a/lib/open_api_spex.ex
+++ b/lib/open_api_spex.ex
@@ -1,98 +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, 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{}, params, content_type \\ nil) do
Operation.cast(operation, params, 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{}, params = %{}, content_type \\ nil) do
Operation.validate(operation, params, 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 Map.keys(@schema.properties)
@type t :: %__MODULE__{}
end
end
end
diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex
index cd29e13..ab98e19 100644
--- a/lib/open_api_spex/schema.ex
+++ b/lib/open_api_spex/schema.ex
@@ -1,379 +1,385 @@
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, true},
: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,
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: [String.t] | nil,
enum: [String.t] | nil,
type: atom,
allOf: [Schema.t | Reference.t] | nil,
oneOf: [Schema.t | Reference.t] | nil,
anyOf: [Schema.t | Reference.t] | nil,
not: Schema.t | Reference.t | nil,
items: Schema.t | Reference.t | nil,
properties: %{atom => Schema.t | Reference.t} | nil,
additionalProperties: boolean | Schema.t | Reference.t | nil,
description: String.t,
format: String.t | nil,
default: any | nil,
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" -> 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(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
@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(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
@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}"}
@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}"}
@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}"}
@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
@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
@spec validate_pattern(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
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
@spec validate_max_items(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
@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
@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
defp validate_unique_items(_, _), do: :ok
@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
@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
@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
@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
@spec validate_object_properties(Enumerable.t, %{}, %{String.t => Schema.t | Reference.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
defp validate_object_properties([], _, _), do: :ok
defp validate_object_properties([{name, schema} | rest], value, schemas = %{}) do
case validate(schema, Map.get(value, name), schemas) do
:ok -> validate_object_properties(rest, value, schemas)
error -> error
end
end
end
\ No newline at end of file

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 23, 10:30 AM (20 h, 52 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
38943
Default Alt Text
(26 KB)

Event Timeline