Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F112294
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
34 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/README.md b/README.md
index 0102857..efaed8e 100644
--- a/README.md
+++ b/README.md
@@ -1,199 +1,210 @@
# 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
@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", Schemas.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:
```elixir
defmodule MyApp.Schemas do
alias OpenApiSpex.Schema
defmodule User do
def schema do
%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}
}
}
end
end
defmodule UserResponse do
def schema do
%Schema{
title: "UserResponse",
description: "Response schema for single user",
type: :object,
properties: %{
data: User
}
}
end
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
```
## 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
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", Schemas.UserRequest),
responses: %{
201 => response("User", "application/json", Schemas.UserResponse)
}
}
end
def create(conn, %{user: %{name: name, email: email, birthday: birthday = %Date{}}}) do
# params will have atom keys with values cast to standard elixir types
end
end
```
-TODO: SwaggerUI 3.0
-TODO: Request Validation
+## 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.
+
+
+TODO: SwaggerUI 3.0
TODO: Validating examples in the spec
TODO: Validating responses in tests
diff --git a/lib/open_api_spex.ex b/lib/open_api_spex.ex
index f4cc5a5..387c12c 100644
--- a/lib/open_api_spex.ex
+++ b/lib/open_api_spex.ex
@@ -1,40 +1,86 @@
defmodule OpenApiSpex do
- alias OpenApiSpex.{OpenApi, RequestBody, Schema, SchemaResolver}
- alias Plug.Conn
+ alias OpenApiSpex.{OpenApi, Operation, Parameter, RequestBody, Schema, SchemaResolver}
@moduledoc """
"""
def resolve_schema_modules(spec = %OpenApi{}) do
SchemaResolver.resolve_schema_modules(spec)
end
- def cast_parameters(conn = %Conn{}, operation_id) do
- operation = conn.private.open_api_spex.operation_lookup[operation_id]
- spec = conn.private.open_api_spex.spec
+ def cast_parameters(spec = %OpenApi{}, operation = %Operation{}, params = %{}, content_type \\ nil) do
schemas = spec.components.schemas
- params =
+ parameters_result =
operation.parameters
- |> Enum.filter(fn parameter -> Map.has_key?(conn.params, Atom.to_string(parameter.name)) end)
- |> Enum.map(fn %{schema: schema, name: name} -> {name, Schema.cast(schema, conn.params[name], schemas)} end)
+ |> Stream.filter(fn parameter -> Map.has_key?(params, Atom.to_string(parameter.name)) end)
+ |> Stream.map(fn parameter -> %{name: parameter.name, schema: Parameter.schema(parameter)} end)
+ |> Stream.map(fn %{schema: schema, name: name} -> {name, Schema.cast(schema, params[name], schemas)} end)
|> Enum.reduce({:ok, %{}}, fn
{name, {:ok, val}}, {:ok, acc} -> {:ok, Map.put(acc, name, val)}
_, {:error, reason} -> {:error, reason}
{_name, {:error, reason}}, _ -> {:error, reason}
end)
- body = case operation.requestBody do
- nil -> {:ok, %{}}
- %RequestBody{content: content} ->
- [content_type] = Conn.get_req_header(conn, "content-type")
- schema = content[content_type].schema
- Schema.cast(schema, conn.params, spec.components.schemas)
- end
+ body_result =
+ case operation.requestBody do
+ nil -> {:ok, %{}}
+ %RequestBody{content: content} ->
+ schema = content[content_type].schema
+ Schema.cast(schema, params, spec.components.schemas)
+ end
- with {:ok, cast_params} <- params,
- {:ok, cast_body} <- body do
+ with {:ok, cast_params} <- parameters_result,
+ {:ok, cast_body} <- body_result do
params = Map.merge(cast_params, cast_body)
- {:ok, %{conn | params: params}}
+ {:ok, params}
end
end
+
+ def validate_parameters(spec = %OpenApi{}, operation = %Operation{}, params = %{}, content_type \\ nil) do
+ schemas = spec.components.schemas
+
+ with :ok <- validate_required_parameters(operation.parameters, params),
+ {:ok, remaining} <- validate_parameter_schemas(operation.parameters, params, schemas),
+ :ok <- validate_body_schema(operation.requestBody, remaining, content_type, schemas) do
+ :ok
+ end
+ end
+
+ def validate_required_parameters(parameter_list, params = %{}) do
+ required =
+ parameter_list
+ |> Stream.filter(fn parameter -> parameter.required end)
+ |> Enum.map(fn parameter -> parameter.name end)
+
+ missing = required -- Map.keys(params)
+ case missing do
+ [] -> :ok
+ _ -> {:error, "Missing required parameters: #{inspect(missing)}"}
+ end
+ end
+
+ def validate_parameter_schemas(parameter_list, params, schemas) do
+ errors =
+ parameter_list
+ |> Stream.filter(fn parameter -> Map.has_key?(params, parameter.name) end)
+ |> Stream.map(fn parameter -> Parameter.schema(parameter) end)
+ |> Stream.map(fn schema -> Schema.validate(schema, params, schemas) end)
+ |> Enum.filter(fn result -> result != :ok end)
+
+ case errors do
+ [] -> {:ok, Map.drop(params, Enum.map(parameter_list, fn p -> p.name end)) }
+ _ -> {:error, "Parameter validation errors: #{inspect(errors)}"}
+ end
+ end
+
+ def validate_body_schema(nil, _, _, _), do: :ok
+ def validate_body_schema(%RequestBody{required: false}, params, _content_type, _schemas) when map_size(params) == 0 do
+ :ok
+ end
+ def validate_body_schema(%RequestBody{content: content}, params, content_type, schemas) do
+ content
+ |> Map.get(content_type)
+ |> Map.get(:schema)
+ |> Schema.validate(params, schemas)
+ end
end
diff --git a/lib/open_api_spex/open_api.ex b/lib/open_api_spex/open_api.ex
index 0322d26..72a0107 100644
--- a/lib/open_api_spex/open_api.ex
+++ b/lib/open_api_spex/open_api.ex
@@ -1,55 +1,56 @@
defmodule OpenApiSpex.OpenApi do
alias OpenApiSpex.{
Info, Server, Paths, Components,
SecurityRequirement, Tag, ExternalDocumentation,
OpenApi
}
defstruct [
:info,
:servers,
:paths,
:components,
:security,
:tags,
:externalDocs,
openapi: "3.0",
]
@type t :: %OpenApi{
openapi: String.t,
info: Info.t,
servers: [Server.t],
paths: Paths.t,
components: Components.t,
security: [SecurityRequirement.t],
tags: [Tag.t],
externalDocs: ExternalDocumentation.t
}
defimpl Poison.Encoder do
def encode(api_spec = %OpenApi{}, options) do
api_spec
|> to_json()
|> Poison.Encoder.encode(options)
end
+ defp to_json(%Regex{source: source}), do: source
defp to_json(value = %{__struct__: _}) do
value
|> Map.from_struct()
|> to_json()
end
defp to_json(value) when is_map(value) do
value
- |> Enum.map(fn {k,v} -> {to_string(k), to_json(v)} end)
- |> Enum.filter(fn {_, nil} -> false; _ -> true end)
+ |> Stream.map(fn {k,v} -> {to_string(k), to_json(v)} end)
+ |> Stream.filter(fn {_, nil} -> false; _ -> true end)
|> Enum.into(%{})
end
defp to_json(value) when is_list(value) do
Enum.map(value, &to_json/1)
end
- defp to_json(nil) do nil end
- defp to_json(true) do true end
- defp to_json(false) do false end
- defp to_json(value) when is_atom(value) do to_string(value) end
- defp to_json(value) do value end
+ defp to_json(nil), do: nil
+ defp to_json(true), do: true
+ defp to_json(false), do: false
+ defp to_json(value) when is_atom(value), do: to_string(value)
+ defp to_json(value), do: value
end
end
\ No newline at end of file
diff --git a/lib/open_api_spex/parameter.ex b/lib/open_api_spex/parameter.ex
index 4d195bd..0cce4ce 100644
--- a/lib/open_api_spex/parameter.ex
+++ b/lib/open_api_spex/parameter.ex
@@ -1,50 +1,58 @@
defmodule OpenApiSpex.Parameter do
alias OpenApiSpex.{
Schema, Reference, Example, MediaType, Parameter
}
defstruct [
:name,
:in,
:description,
:required,
:deprecated,
:allowEmptyValue,
:style,
:explode,
:allowReserved,
:schema,
:example,
:examples,
:content,
]
@type style :: :matrix | :label | :form | :simple | :spaceDelimited | :pipeDelimited | :deep
@type t :: %__MODULE__{
name: String.t,
in: :query | :header | :path | :cookie,
description: String.t,
required: boolean,
deprecated: boolean,
allowEmptyValue: boolean,
style: style,
explode: boolean,
allowReserved: boolean,
schema: Schema.t | Reference.t,
example: any,
examples: %{String.t => Example.t | Reference.t},
content: %{String.t => MediaType.t}
}
@doc """
Sets the schema for a parameter from a simple type, reference or Schema
"""
@spec put_schema(t, Reference.t | Schema.t | atom | String.t) :: t
def put_schema(parameter = %Parameter{}, type = %Reference{}) do
%{parameter | schema: type}
end
def put_schema(parameter = %Parameter{}, type = %Schema{}) do
%{parameter | schema: type}
end
def put_schema(parameter = %Parameter{}, type) when is_binary(type) or is_atom(type) do
%{parameter | schema: %Schema{type: type}}
end
+
+ def schema(%Parameter{schema: schema = %{}}) do
+ schema
+ end
+ def schema(%Parameter{content: content = %{}}) do
+ {_type, %MediaType{schema: schema}} = Enum.at(content, 0)
+ schema
+ end
end
diff --git a/lib/open_api_spex/plug/cast.ex b/lib/open_api_spex/plug/cast.ex
index 7daab11..7bb6b63 100644
--- a/lib/open_api_spex/plug/cast.ex
+++ b/lib/open_api_spex/plug/cast.ex
@@ -1,15 +1,24 @@
defmodule OpenApiSpex.Plug.Cast do
+ alias Plug.Conn
+
def init(opts), do: opts
- def call(conn, operation_id: operation_id) do
- case OpenApiSpex.cast_parameters(conn, operation_id) do
- {:ok, conn} -> conn
+
+ def call(conn = %{private: %{open_api_spex: private_data}}, operation_id: operation_id) do
+ spec = private_data.spec
+ operation = private_data.operation_lookup[operation_id]
+ content_type = Conn.get_req_header(conn, "content-type") |> Enum.at(0)
+ private_data = Map.put(private_data, :operation_id, operation_id)
+ conn = Conn.put_private(conn, :open_api_spex, private_data)
+
+ case OpenApiSpex.cast_parameters(spec, operation, conn.params, content_type) do
+ {:ok, params} -> %{conn | params: params}
{:error, reason} ->
conn
|> Plug.Conn.send_resp(422, "#{reason}")
|> Plug.Conn.halt()
end
end
def call(conn = %{private: %{phoenix_controller: controller, phoenix_action: action}}, _opts) do
call(conn, operation_id: controller.open_api_operation(action).operationId)
end
end
\ No newline at end of file
diff --git a/lib/open_api_spex/plug/validate.ex b/lib/open_api_spex/plug/validate.ex
new file mode 100644
index 0000000..a6ee703
--- /dev/null
+++ b/lib/open_api_spex/plug/validate.ex
@@ -0,0 +1,21 @@
+defmodule OpenApiSpex.Plug.Validate do
+ alias Plug.Conn
+
+ def init(opts), do: opts
+ def call(conn, _opts) do
+ spec = conn.private.open_api_spex.spec
+ operation_id = conn.private.open_api_spex.operation_id
+ operation_lookup = conn.private.open_api_spex.operation_lookup
+ operation = operation_lookup[operation_id]
+ content_type = Conn.get_req_header(conn, "content-type") |> Enum.at(0)
+
+ with :ok <- OpenApiSpex.validate_parameters(spec, operation, conn.params, content_type) do
+ conn
+ else
+ {:error, reason} ->
+ conn
+ |> Conn.send_resp(422, "#{reason}")
+ |> Conn.halt()
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/request_body.ex b/lib/open_api_spex/request_body.ex
index 0bf2255..134b899 100644
--- a/lib/open_api_spex/request_body.ex
+++ b/lib/open_api_spex/request_body.ex
@@ -1,13 +1,13 @@
defmodule OpenApiSpex.RequestBody do
alias OpenApiSpex.MediaType
defstruct [
:description,
:content,
- :required
+ required: false
]
@type t :: %__MODULE__{
description: String.t,
content: %{String.t => MediaType.t},
required: boolean
}
end
\ No newline at end of file
diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex
index 1617be3..4470776 100644
--- a/lib/open_api_spex/schema.ex
+++ b/lib/open_api_spex/schema.ex
@@ -1,131 +1,292 @@
defmodule OpenApiSpex.Schema do
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,
:description,
:format,
:default,
:nullable,
:discriminator,
:readOnly,
:writeOnly,
:xml,
:externalDocs,
:example,
:deprecated
]
@type t :: %__MODULE__{
title: String.t,
multipleOf: number,
maximum: number,
- exclusiveMaximum: number,
+ exclusiveMaximum: boolean,
minimum: number,
- exclusiveMinimum: 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
}
+ defp resolve_schema(schema = %Schema{}, _), do: schema
+ defp resolve_schema(%Reference{"$ref": "#/components/schemas/" <> name}, schemas), do: schemas[name]
+
+ 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
+ case cast(items_schema, x, schemas) do
+ {:ok, x_cast} -> [x_cast | cast(schema, rest, schemas)]
+ error -> error
+ end
+ end
def cast(%Schema{type: :object, properties: properties}, value, schemas) when is_map(value) do
properties
|> Stream.filter(fn {name, _} -> Map.has_key?(value, name) || Map.has_key?(value, Atom.to_string(name)) end)
|> Stream.map(fn {name, schema} -> {name, resolve_schema(schema, schemas)} end)
|> Stream.map(fn {name, schema} -> {name, schema, Map.get(value, name, value[Atom.to_string(name)])} end)
|> Stream.map(fn {name, schema, property_val} -> cast_property(name, schema, property_val, schemas) end)
|> Enum.reduce({:ok, %{}}, fn
_, {:error, reason} -> {:error, reason}
{:error, reason}, _ -> {:error, reason}
{:ok, {name, property_val}}, {:ok, acc} -> {:ok, Map.put(acc, name, property_val)}
end)
end
def cast(ref = %Reference{}, val, schemas), do: cast(resolve_schema(ref, schemas), val, schemas)
- defp resolve_schema(schema = %Schema{}, _), do: schema
- defp resolve_schema(%Reference{"$ref": "#/components/schemas/" <> name}, schemas), do: schemas[name]
-
defp cast_property(name, schema, value, schemas) do
casted = cast(schema, value, schemas)
case casted do
{:ok, new_value} -> {:ok, {name, new_value}}
{:error, reason} -> {:error, reason}
end
end
+
+ def validate(ref = %Reference{}, val, schemas), do: validate(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}"}
+
+ 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}"}
+
+ 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}"}
+
+ def validate_max_length(%{maxLength: nil}, _), do: :ok
+ def 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
+ 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)
+ end
+ def 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
+ {: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
+ {:error, "Array length #{length(value)} is smaller than minItems: #{n}"}
+ end
+
+ def 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_array_items(%Schema{type: :array, items: nil}, value, _schemas) when is_list(value), do: {:ok, value}
+ 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
+ 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
+ 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
+ {: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
+ {:error, "Object property count #{map_size(val)} is less than minProperties: #{n}"}
+ end
+
+ def 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
+ case validate(schema, 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/open_api_spex_test.exs b/test/open_api_spex_test.exs
index 387ed3c..7e24f47 100644
--- a/test/open_api_spex_test.exs
+++ b/test/open_api_spex_test.exs
@@ -1,37 +1,57 @@
defmodule OpenApiSpexTest do
use ExUnit.Case
alias OpenApiSpexTest.ApiSpec
describe "OpenApi" do
test "compete" do
spec = ApiSpec.spec()
assert spec
end
- test "asdfafd" 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", Poison.encode!(request_body))
|> Plug.Conn.put_req_header("content-type", "application/json")
conn = OpenApiSpexTest.Router.call(conn, [])
assert conn.params == %{
user: %{
id: 123,
name: "asdf",
email: "foo@bar.com",
updated_at: ~N[2017-09-12T14:44:55Z] |> DateTime.from_naive!("Etc/UTC")
}
}
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", Poison.encode!(request_body))
+ |> Plug.Conn.put_req_header("content-type", "application/json")
+
+ conn = OpenApiSpexTest.Router.call(conn, [])
+ assert conn.status == 422
+ assert conn.resp_body == "Value does not match pattern: [a-zA-Z][a-zA-Z0-9_]+"
+ end
end
end
\ No newline at end of file
diff --git a/test/support/router.ex b/test/support/router.ex
index 1ae80fa..947fc6a 100644
--- a/test/support/router.ex
+++ b/test/support/router.ex
@@ -1,14 +1,18 @@
defmodule OpenApiSpexTest.Router do
use Phoenix.Router
+ alias Plug.Parsers
+ alias OpenApiSpexTest.UserController
+ alias OpenApiSpex.Plug.{PutApiSpec, RenderSpec}
pipeline :api do
- plug OpenApiSpex.Plug.PutApiSpec, module: OpenApiSpexTest.ApiSpec
- plug Plug.Parsers, parsers: [:json], pass: ["text/*"], json_decoder: Poison
+ plug :accepts, ["json"]
+ plug PutApiSpec, module: OpenApiSpexTest.ApiSpec
+ plug Parsers, parsers: [:json], pass: ["text/*"], json_decoder: Poison
end
scope "/api" do
pipe_through :api
- resources "/users", OpenApiSpexTest.UserController, only: [:create, :index, :show]
- get "/openapi", OpenApiSpex.Plug.RenderSpec, []
+ resources "/users", UserController, only: [:create, :index, :show]
+ get "/openapi", RenderSpec, []
end
end
\ No newline at end of file
diff --git a/test/support/schemas.ex b/test/support/schemas.ex
index 19e542f..9a839cf 100644
--- a/test/support/schemas.ex
+++ b/test/support/schemas.ex
@@ -1,59 +1,59 @@
defmodule OpenApiSpexTest.Schemas do
alias OpenApiSpex.Schema
defmodule User do
def schema do
%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"},
+ 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'}
}
}
end
end
defmodule UserRequest do
def schema do
%Schema{
title: "UserRequest",
description: "POST body for creating a user",
type: :object,
properties: %{
user: User
}
}
end
end
defmodule UserResponse do
def schema do
%Schema{
title: "UserResponse",
description: "Response schema for single user",
type: :object,
properties: %{
data: User
}
}
end
end
defmodule UsersResponse do
def schema do
%Schema{
title: "UsersReponse",
description: "Response schema for multiple users",
type: :object,
properties: %{
data: %Schema{description: "The users details", type: :array, items: User}
}
}
end
end
end
\ No newline at end of file
diff --git a/test/support/user_controller.ex b/test/support/user_controller.ex
index 69c5ee9..175771e 100644
--- a/test/support/user_controller.ex
+++ b/test/support/user_controller.ex
@@ -1,69 +1,70 @@
defmodule OpenApiSpexTest.UserController do
use Phoenix.Controller
alias OpenApiSpex.Operation
alias OpenApiSpexTest.Schemas
alias Plug.Conn
plug OpenApiSpex.Plug.Cast
+ plug OpenApiSpex.Plug.Validate
def open_api_operation(action) do
apply(__MODULE__, :"#{action}_operation", [])
end
def show_operation() do
import Operation
%Operation{
tags: ["users"],
summary: "Show user",
description: "Show a user by ID",
operationId: "UserController.show",
parameters: [
parameter(:id, :path, :integer, "User ID", example: 123)
],
responses: %{
200 => response("User", "application/json", Schemas.UserResponse)
}
}
end
def show(conn, _params) do
conn
|> Conn.send_resp(200, "HELLO")
end
def index_operation() do
import Operation
%Operation{
tags: ["users"],
summary: "List users",
description: "List all useres",
operationId: "UserController.index",
parameters: [],
responses: %{
200 => response("User List Response", "application/json", Schemas.UsersResponse)
}
}
end
def index(conn, _params) do
conn
|> Conn.send_resp(200, "HELLO")
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", Schemas.UserRequest),
responses: %{
201 => response("User", "application/json", Schemas.UserResponse)
}
}
end
def create(conn, _params) do
conn
|> Conn.send_resp(201, "DONE")
end
end
\ No newline at end of file
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Nov 23, 7:39 AM (1 d, 5 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
38900
Default Alt Text
(34 KB)
Attached To
Mode
R22 open_api_spex
Attached
Detach File
Event Timeline
Log In to Comment