Page MenuHomePhorge

No OneTemporary

Size
14 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/open_api_spex.ex b/lib/open_api_spex.ex
index a646699..3bc5d29 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, 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 = %{}, 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{}, params = %{}, content_type \\ nil) do
- Operation.validate(operation, params, content_type, spec.components.schemas)
+ def validate(spec = %OpenApi{}, operation = %Operation{}, 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()
@type t :: %__MODULE__{}
end
end
end
diff --git a/lib/open_api_spex/operation.ex b/lib/open_api_spex/operation.ex
index 5d6d33c..aef56fa 100644
--- a/lib/open_api_spex/operation.ex
+++ b/lib/open_api_spex/operation.ex
@@ -1,222 +1,220 @@
defmodule OpenApiSpex.Operation do
@moduledoc """
Defines the `OpenApiSpex.Operation.t` type.
"""
alias OpenApiSpex.{
Callback,
ExternalDocumentation,
MediaType,
Operation,
Parameter,
Reference,
RequestBody,
Response,
Responses,
Schema,
SecurityRequirement,
Server,
}
defstruct [
:tags,
:summary,
:description,
:externalDocs,
:operationId,
:parameters,
:requestBody,
:responses,
:callbacks,
:deprecated,
:security,
:servers
]
@typedoc """
[Operation Object](https://swagger.io/specification/#operationObject)
Describes a single API operation on a path.
"""
@type t :: %__MODULE__{
tags: [String.t] | nil,
summary: String.t | nil,
description: String.t | nil,
externalDocs: ExternalDocumentation.t | nil,
operationId: String.t | nil,
parameters: [Parameter.t | Reference.t] | nil,
requestBody: [RequestBody.t | Reference.t] | nil,
responses: Responses.t,
callbacks: %{String.t => Callback.t | Reference.t} | nil,
deprecated: boolean | nil,
security: [SecurityRequirement.t] | nil,
servers: [Server.t] | nil
}
@doc """
Constructs an Operation struct from the plug and opts specified in the given route
"""
@spec from_route(PathItem.route) :: t
def from_route(route) do
from_plug(route.plug, route.opts)
end
@doc """
Constructs an Operation struct from plug module and opts
"""
@spec from_plug(module, any) :: t
def from_plug(plug, opts) do
plug.open_api_operation(opts)
end
@doc """
Shorthand for constructing a Parameter name, location, type, description and optional examples
"""
@spec parameter(atom, Parameter.location, Reference.t | Schema.t | atom, String.t, keyword) :: Parameter.t
def parameter(name, location, type, description, opts \\ []) do
params =
[name: name, in: location, description: description, required: location == :path]
|> Keyword.merge(opts)
Parameter
|> struct(params)
|> Parameter.put_schema(type)
end
@doc """
Shorthand for constructing a RequestBody with description, media_type, schema and optional examples
"""
@spec request_body(String.t, String.t, (Schema.t | Reference.t | module), keyword) :: RequestBody.t
def request_body(description, media_type, schema_ref, opts \\ []) do
%RequestBody{
description: description,
content: %{
media_type => %MediaType{
schema: schema_ref,
example: opts[:example],
examples: opts[:examples]
}
},
required: opts[:required] || false
}
end
@doc """
Shorthand for constructing a Response with description, media_type, schema and optional examples
"""
@spec response(String.t, String.t, (Schema.t | Reference.t | module), keyword) :: Response.t
def response(description, media_type, schema_ref, opts \\ []) do
%Response{
description: description,
content: %{
media_type => %MediaType {
schema: schema_ref,
example: opts[:example],
examples: opts[:examples]
}
}
}
end
@doc """
Cast params to the types defined by the schemas of the operation parameters and requestBody
"""
@spec cast(Operation.t, Conn.t, String.t | nil, %{String.t => Schema.t}) :: {:ok, map} | {:error, String.t}
def cast(operation = %Operation{}, conn = %{}, content_type, schemas) do
parameters = Enum.filter(operation.parameters || [], fn p -> Map.has_key?(conn.params, Atom.to_string(p.name)) end)
with :ok <- validate_parameters(conn, operation.parameters),
{:ok, parameter_values} <- cast_parameters(parameters, conn.params, schemas),
{:ok, body} <- cast_request_body(operation.requestBody, conn.params, content_type, schemas) do
{:ok, Map.merge(parameter_values, body)}
end
end
@spec validate_parameters(Conn.t, list | nil) :: :ok | {:error, String.t}
defp validate_parameters(%Plug.Conn{} = conn, defined_params) when is_nil(defined_params) do
case conn.query_params do
%{} -> :ok
_ -> {:error, "No query parameters defined for this operation"}
end
end
defp validate_parameters(%Plug.Conn{} = conn, defined_params) when is_list(defined_params) do
defined_query_params = for param <- defined_params, param.in == :query, into: MapSet.new(), do: to_string(param.name)
case validate_parameter_keys(Map.keys(conn.query_params), defined_query_params) do
{:error, param} -> {:error, "Undefined query parameter: #{inspect(param)}"}
:ok -> :ok
end
end
@spec validate_parameter_keys([String.t], MapSet.t) :: {:error, String.t} | :ok
defp validate_parameter_keys([], _defined_params), do: :ok
defp validate_parameter_keys([param|params], defined_params) do
case MapSet.member?(defined_params, param) do
false -> {:error, param}
_ -> validate_parameter_keys(params, defined_params)
end
end
-
@spec cast_parameters([Parameter.t], map, %{String.t => Schema.t}) :: {:ok, map} | {:error, String.t}
defp cast_parameters([], _params, _schemas), do: {:ok, %{}}
defp cast_parameters([p | rest], params = %{}, schemas) do
with {:ok, cast_val} <- Schema.cast(Parameter.schema(p), params[Atom.to_string(p.name)], schemas),
{:ok, cast_tail} <- cast_parameters(rest, params, schemas) do
{:ok, Map.put_new(cast_tail, p.name, cast_val)}
end
end
@spec cast_request_body(RequestBody.t | nil, map, String.t | nil, %{String.t => Schema.t}) :: {:ok, map} | {:error, String.t}
defp cast_request_body(nil, _, _, _), do: {:ok, %{}}
defp cast_request_body(%RequestBody{content: content}, params, content_type, schemas) do
schema = content[content_type].schema
Schema.cast(schema, params, schemas)
end
-
@doc """
Validate params against the schemas of the operation parameters and requestBody
"""
- @spec validate(Operation.t, map, String.t | nil, %{String.t => Schema.t}) :: :ok | {:error, String.t}
- def validate(operation = %Operation{}, params = %{}, content_type, schemas) do
- with :ok <- validate_required_parameters(operation.parameters || [], params),
- parameters <- Enum.filter(operation.parameters || [], &Map.has_key?(params, &1.name)),
- {:ok, remaining} <- validate_parameter_schemas(parameters, params, schemas),
+ @spec validate(Operation.t, Conn.t, String.t | nil, %{String.t => Schema.t}) :: :ok | {:error, String.t}
+ def validate(operation = %Operation{}, conn = %{}, content_type, schemas) do
+ with :ok <- validate_required_parameters(operation.parameters || [], conn.params),
+ parameters <- Enum.filter(operation.parameters || [], &Map.has_key?(conn.params, &1.name)),
+ {:ok, remaining} <- validate_parameter_schemas(parameters, conn.params, schemas),
:ok <- validate_body_schema(operation.requestBody, remaining, content_type, schemas) do
:ok
end
end
@spec validate_required_parameters([Parameter.t], map) :: :ok | {:error, String.t}
defp 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
@spec validate_parameter_schemas([Parameter.t], map, %{String.t => Schema.t}) :: {:ok, map} | {:error, String.t}
defp validate_parameter_schemas([], params, _schemas), do: {:ok, params}
defp validate_parameter_schemas([p | rest], params, schemas) do
with :ok <- Schema.validate(Parameter.schema(p), params[p.name], schemas),
{:ok, remaining} <- validate_parameter_schemas(rest, params, schemas) do
{:ok, Map.delete(remaining, p.name)}
end
end
@spec validate_body_schema(RequestBody.t | nil, map, String.t | nil, %{String.t => Schema.t}) :: :ok | {:error, String.t}
defp validate_body_schema(nil, _, _, _), do: :ok
defp validate_body_schema(%RequestBody{required: false}, params, _content_type, _schemas) when map_size(params) == 0 do
:ok
end
defp 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/plug/validate.ex b/lib/open_api_spex/plug/validate.ex
index 52fd2cf..3a9e46e 100644
--- a/lib/open_api_spex/plug/validate.ex
+++ b/lib/open_api_spex/plug/validate.ex
@@ -1,42 +1,42 @@
defmodule OpenApiSpex.Plug.Validate do
@moduledoc """
Module plug that validates params against the schema defined for an operation.
If validation fails, the plug will send a 422 response with the reason as the body.
This plug should always be run after `OpenApiSpex.Plug.Cast`, as it expects the params map to
have atom keys and query params converted from strings to the appropriate types.
## Example
defmodule MyApp.UserController do
use Phoenix.Controller
plug OpenApiSpex.Plug.Cast
plug OpenApiSpex.Plug.Validate
...
end
"""
@behaviour Plug
alias Plug.Conn
@impl Plug
def init(opts), do: opts
@impl Plug
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(spec, operation, conn.params, content_type) do
+ with :ok <- OpenApiSpex.validate(spec, operation, conn, content_type) do
conn
else
{:error, reason} ->
conn
|> Conn.send_resp(422, "#{reason}")
|> Conn.halt()
end
end
end
\ No newline at end of file

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 23, 2:26 PM (1 d, 1 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39002
Default Alt Text
(14 KB)

Event Timeline