Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F112448
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
14 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
R22 open_api_spex
Attached
Detach File
Event Timeline
Log In to Comment