Page MenuHomePhorge

No OneTemporary

Size
12 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/open_api_spex/operation.ex b/lib/open_api_spex/operation.ex
index e9a1627..9009701 100644
--- a/lib/open_api_spex/operation.ex
+++ b/lib/open_api_spex/operation.ex
@@ -1,221 +1,233 @@
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: nil,
description: nil,
externalDocs: nil,
operationId: nil,
parameters: [],
requestBody: nil,
responses: nil,
callbacks: %{},
deprecated: false,
security: nil,
servers: nil
@typedoc """
[Operation Object](https://swagger.io/specification/#operationObject)
Describes a single API operation on a path.
"""
@type t :: %__MODULE__{
tags: [String.t()],
summary: String.t() | nil,
description: String.t() | nil,
externalDocs: ExternalDocumentation.t() | nil,
operationId: String.t() | nil,
parameters: [Parameter.t() | Reference.t()],
requestBody: RequestBody.t() | Reference.t() | nil,
responses: Responses.t(),
callbacks: %{String.t() => Callback.t() | Reference.t()},
deprecated: boolean,
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)
+ def from_route(route)
+
+ def from_route(route = %_{}) do
+ route
+ |> Map.from_struct()
+ |> from_route()
+ end
+
+ def from_route(%{plug: plug, plug_opts: opts}) do
+ from_plug(plug, opts)
+ end
+
+ def from_route(%{plug: plug, opts: opts}) do
+ from_plug(plug, 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
diff --git a/lib/open_api_spex/path_item.ex b/lib/open_api_spex/path_item.ex
index 8e893bf..670513e 100644
--- a/lib/open_api_spex/path_item.ex
+++ b/lib/open_api_spex/path_item.ex
@@ -1,72 +1,74 @@
defmodule OpenApiSpex.PathItem do
@moduledoc """
Defines the `OpenApiSpex.PathItem.t` type.
"""
alias OpenApiSpex.{Operation, Server, Parameter, PathItem, Reference}
defstruct [
:"$ref",
:summary,
:description,
:get,
:put,
:post,
:delete,
:options,
:head,
:patch,
:trace,
:servers,
:parameters
]
@typedoc """
[Path Item Object](https://swagger.io/specification/#pathItemObject)
Describes the operations available on a single path.
A Path Item MAY be empty, due to ACL constraints.
The path itself is still exposed to the documentation viewer
but they will not know which operations and parameters are available.
"""
@type t :: %__MODULE__{
"$ref": String.t | nil,
summary: String.t | nil,
description: String.t | nil,
get: Operation.t | nil,
put: Operation.t | nil,
post: Operation.t | nil,
delete: Operation.t | nil,
options: Operation.t | nil,
head: Operation.t | nil,
patch: Operation.t | nil,
trace: Operation.t | nil,
servers: [Server.t] | nil,
parameters: [Parameter.t | Reference.t] | nil
}
@typedoc """
Represents a route from in a Plug/Phoenix application.
Eg from the generated `__routes__` function in a Phoenix.Router module.
"""
- @type route :: %{verb: atom, plug: atom, opts: any}
+ @type route ::
+ %{verb: atom, plug: atom, opts: any} |
+ %{verb: atom, plug: atom, plug_opts: any}
@doc """
Builds a PathItem struct from a list of routes that share a path.
"""
@spec from_routes([route]) :: nil | t
def from_routes(routes) do
Enum.each(routes, fn route ->
Code.ensure_loaded(route.plug)
end)
routes
|> Enum.filter(&function_exported?(&1.plug, :open_api_operation, 1))
|> from_valid_routes()
end
@spec from_valid_routes([route]) :: nil | t
defp from_valid_routes([]), do: nil
defp from_valid_routes(routes) do
struct(PathItem, Enum.map(routes, &{&1.verb, Operation.from_route(&1)}))
end
end
diff --git a/test/operation_test.exs b/test/operation_test.exs
index 5800126..642973d 100644
--- a/test/operation_test.exs
+++ b/test/operation_test.exs
@@ -1,16 +1,36 @@
defmodule OpenApiSpex.OperationTest do
use ExUnit.Case
alias OpenApiSpex.Operation
alias OpenApiSpexTest.UserController
describe "Operation" do
- test "from_route" do
- route = %{plug: UserController, opts: :show}
+ test "from_route %Phoenix.Router.Route{}" do
+ plug = UserController
+ plug_opts = :show
+
+ route = Phoenix.Router.Route.build(
+ _line = nil,
+ _kind = :match,
+ _verb = :atom,
+ _path = nil,
+ _host = nil,
+ plug,
+ plug_opts,
+ _helper = nil,
+ _pipe_through = [],
+ _private = %{},
+ _assigns = %{}
+ )
+ assert Operation.from_route(route) == UserController.show_operation()
+ end
+
+ test "from_route ~ phoenix v1.4.7+" do
+ route = %{plug: UserController, plug_opts: :show}
assert Operation.from_route(route) == UserController.show_operation()
end
test "from_plug" do
assert Operation.from_plug(UserController, :show) == UserController.show_operation()
end
end
-end
\ No newline at end of file
+end

File Metadata

Mime Type
text/x-diff
Expires
Thu, Nov 28, 6:24 PM (1 d, 17 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41081
Default Alt Text
(12 KB)

Event Timeline