Packages
open_api_spex
3.1.0
3.22.3
3.22.2
3.22.1
3.22.0
3.21.5
3.21.4
3.21.3
3.21.2
3.21.1
3.21.0
3.20.1
3.20.0
3.19.1
3.19.0
3.18.3
3.18.2
3.18.1
3.18.0
3.17.3
3.17.2
3.17.1
3.17.0
3.16.4
3.16.3
3.16.2
3.16.1
3.16.0
3.15.0
3.14.0
3.13.0
3.12.0
3.11.0
3.10.0
3.9.0
3.8.0
3.7.0
3.6.0
3.5.2
3.5.1
3.5.0
3.4.0
3.3.0
3.2.1
3.2.0
3.1.0
3.0.0
2.3.1
2.3.0
2.2.0
2.1.1
2.1.0
2.0.0
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.1
1.0.0
Leverage Open Api Specification 3 (swagger) to document, test, validate and explore your Plug and Phoenix APIs.
Current section
Files
Jump to
Current section
Files
lib/open_api_spex/operation.ex
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,
}
@enforce_keys :responses
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, Plug.Conn.t} | {:error, String.t}
def cast(operation = %Operation{}, conn = %Plug.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 <- check_query_params_defined(conn, operation.parameters),
{:ok, parameter_values} <- cast_parameters(parameters, conn.params, schemas),
{:ok, body} <- cast_request_body(operation.requestBody, conn.body_params, content_type, schemas) do
{:ok, %{conn | params: parameter_values, body_params: body}}
end
end
@spec check_query_params_defined(Conn.t, list | nil) :: :ok | {:error, String.t}
defp check_query_params_defined(%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 check_query_params_defined(%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, Conn.t, String.t | nil, %{String.t => Schema.t}) :: :ok | {:error, String.t}
def validate(operation = %Operation{}, conn = %Plug.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 <- validate_parameter_schemas(parameters, conn.params, schemas),
:ok <- validate_body_schema(operation.requestBody, conn.body_params, 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 | {:error, String.t()}
defp validate_parameter_schemas([], %{} = _params, _schemas), do: :ok
defp validate_parameter_schemas([p | rest], %{} = params, schemas) do
{:ok, parameter_value} = Map.fetch(params, p.name)
with :ok <- Schema.validate(Parameter.schema(p), parameter_value, schemas) do
validate_parameter_schemas(rest, params, schemas)
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