Page MenuHomePhorge

No OneTemporary

Size
14 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/open_api_spex/cast_parameters.ex b/lib/open_api_spex/cast_parameters.ex
index b376515..41c8c34 100644
--- a/lib/open_api_spex/cast_parameters.ex
+++ b/lib/open_api_spex/cast_parameters.ex
@@ -1,45 +1,97 @@
defmodule OpenApiSpex.CastParameters do
@moduledoc false
alias OpenApiSpex.{Cast, Operation, Parameter, Schema, Reference, Components}
alias OpenApiSpex.Cast.{Error, Object}
alias Plug.Conn
@spec cast(Plug.Conn.t(), Operation.t(), Components.t()) ::
{:error, [Error.t()]} | {:ok, Conn.t()}
def cast(conn, operation, components) do
- # Taken together as a set, operation parameters are similar to an object schema type.
- # Convert parameters to an object schema, then delegate to `Cast.Object.cast/1`
-
- # Operation's parameters list may include references - resolving here
- resolved_parameters =
- Enum.map(operation.parameters, fn
- ref = %Reference{} -> Reference.resolve_parameter(ref, components.parameters)
- param = %Parameter{} -> param
+ # Convert parameters to an object schema for the set location
+ casted_params =
+ operation.parameters
+ |> Enum.reduce(%{}, fn param, acc ->
+ # Operation's parameters list may include references - resolving here
+ parameter =
+ case param do
+ %Reference{} -> Reference.resolve_parameter(param, components.parameters)
+ %Parameter{} -> param
+ end
+
+ location_schema =
+ acc
+ |> Map.get(parameter.in)
+ |> add_to_location_schema(parameter)
+
+ Map.put(acc, parameter.in, location_schema)
end)
+ # then delegate to `Cast.Object.cast/1`
+ |> Enum.map(fn {location, location_schema} ->
+ params = get_params_by_location(conn, location, Map.keys(location_schema.properties))
- properties =
- resolved_parameters
- |> Enum.map(fn parameter -> {parameter.name, Parameter.schema(parameter)} end)
- |> Map.new()
+ ctx = %Cast{
+ value: params,
+ schema: location_schema,
+ schemas: components.schemas
+ }
- required =
- resolved_parameters
- |> Enum.filter(& &1.required)
- |> Enum.map(& &1.name)
+ Object.cast(ctx)
+ end)
+
+ full_cast_result =
+ Enum.reduce_while(casted_params, {:ok, %{}}, fn
+ {:ok, entry}, {:ok, acc} -> {:cont, {:ok, Map.merge(acc, entry)}}
+ cast_error, acc -> {:halt, cast_error}
+ end)
+
+ case full_cast_result do
+ {:ok, result} -> {:ok, %{conn | params: result}}
+ err -> err
+ end
+ end
+
+ defp get_params_by_location(conn, :query, _) do
+ conn.query_params
+ end
- object_schema = %Schema{
+ defp get_params_by_location(conn, :path, _) do
+ conn.path_params
+ end
+
+ defp get_params_by_location(conn, :cookie, _) do
+ conn.req_cookies
+ end
+
+ defp get_params_by_location(conn, :header, expected_names) do
+ conn.req_headers
+ |> Enum.filter(fn {key, value} ->
+ Enum.member?(expected_names, String.downcase(key))
+ end)
+ |> Map.new()
+ end
+
+ defp add_to_location_schema(nil, parameter) do
+ # Since there is no Schema on the "parameter" level, we create one here
+ template_schema = %Schema{
type: :object,
- properties: properties,
- required: required,
- additionalProperties: false
+ additionalProperties: false,
+ properties: %{},
+ required: []
}
- params = Map.merge(conn.path_params, conn.query_params)
+ add_to_location_schema(template_schema, parameter)
+ end
- ctx = %Cast{value: params, schema: object_schema, schemas: components.schemas}
+ defp add_to_location_schema(location_schema, parameter) do
+ # Put the operation parameter to the proper location schema for validation
+ required =
+ case parameter.required do
+ true -> [parameter.name | location_schema.required]
+ _ -> location_schema.required
+ end
- with {:ok, params} <- Object.cast(ctx) do
- {:ok, %{conn | params: params}}
- end
+ properties = Map.put(location_schema.properties, parameter.name, Parameter.schema(parameter))
+
+ location_schema = %{location_schema | properties: properties, required: required}
end
end
diff --git a/test/plug/cast_test.exs b/test/plug/cast_test.exs
index 07a544a..c2cdd61 100644
--- a/test/plug/cast_test.exs
+++ b/test/plug/cast_test.exs
@@ -1,222 +1,238 @@
defmodule OpenApiSpex.Plug.CastTest do
use ExUnit.Case
describe "query params - basics" do
test "Valid Param" do
conn =
:get
|> Plug.Test.conn("/api/users?validParam=true")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 200
end
test "Invalid value" do
conn =
:get
|> Plug.Test.conn("/api/users?validParam=123")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 422
end
test "Invalid Param" do
conn =
:get
|> Plug.Test.conn("/api/users?validParam=123")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 422
error_resp = Jason.decode!(conn.resp_body)
assert error_resp == %{
"errors" => [
%{
"message" => "Invalid boolean. Got: string",
"source" => %{"pointer" => "/validParam"},
"title" => "Invalid value"
}
]
}
end
test "with requestBody" do
body =
Jason.encode!(%{
phone_number: "123-456-789",
postal_address: "123 Lane St"
})
conn =
:post
|> Plug.Test.conn("/api/users/123/contact_info", body)
|> Plug.Conn.put_req_header("content-type", "application/json")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 200
end
end
describe "query params - param with custom error handling" do
test "Valid Param" do
conn =
:get
|> Plug.Test.conn("/api/custom_error_users?validParam=true")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 200
end
test "Invalid value" do
conn =
:get
|> Plug.Test.conn("/api/custom_error_users?validParam=123")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 400
end
test "Invalid Param" do
conn =
:get
|> Plug.Test.conn("/api/custom_error_users?validParam=123")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 400
assert conn.resp_body == "Invalid boolean. Got: string"
end
end
describe "body params" 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", Jason.encode!(request_body))
|> Plug.Conn.put_req_header("content-type", "application/json; charset=UTF-8")
|> OpenApiSpexTest.Router.call([])
assert conn.body_params == %OpenApiSpexTest.Schemas.UserRequest{
user: %OpenApiSpexTest.Schemas.User{
id: 123,
name: "asdf",
email: "foo@bar.com",
updated_at: ~N[2017-09-12T14:44:55Z] |> DateTime.from_naive!("Etc/UTC")
}
}
assert Jason.decode!(conn.resp_body) == %{
"data" => %{
"email" => "foo@bar.com",
"id" => 1234,
"inserted_at" => nil,
"name" => "asdf",
"updated_at" => "2017-09-12T14:44:55Z"
}
}
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", Jason.encode!(request_body))
|> Plug.Conn.put_req_header("content-type", "application/json")
conn = OpenApiSpexTest.Router.call(conn, [])
assert conn.status == 422
resp_data = Jason.decode!(conn.resp_body)
assert resp_data ==
%{
"errors" => [
%{
"message" => "Invalid format. Expected ~r/[a-zA-Z][a-zA-Z0-9_]+/",
"source" => %{"pointer" => "/user/name"},
"title" => "Invalid value"
}
]
}
end
end
describe "oneOf body params" do
test "Valid Request" do
request_body = %{
"pet" => %{
"pet_type" => "Dog",
"bark" => "woof"
}
}
conn =
:post
|> Plug.Test.conn("/api/pets", Jason.encode!(request_body))
|> Plug.Conn.put_req_header("content-type", "application/json; charset=UTF-8")
|> OpenApiSpexTest.Router.call([])
assert conn.body_params == %OpenApiSpexTest.Schemas.PetRequest{
pet: %OpenApiSpexTest.Schemas.Dog{
pet_type: "Dog",
bark: "woof"
}
}
assert Jason.decode!(conn.resp_body) == %{
"data" => %{
"pet_type" => "Dog",
"bark" => "woof"
}
}
end
@tag :capture_log
test "Invalid Request" do
request_body = %{
"pet" => %{
"pet_type" => "Human",
"says" => "yes"
}
}
conn =
:post
|> Plug.Test.conn("/api/pets", Jason.encode!(request_body))
|> Plug.Conn.put_req_header("content-type", "application/json")
conn = OpenApiSpexTest.Router.call(conn, [])
assert conn.status == 422
resp_body = Jason.decode!(conn.resp_body)
assert resp_body == %{
"errors" => [
%{
"source" => %{
"pointer" => "/pet"
},
"title" => "Invalid value",
"message" => "Failed to cast value to one of: [] (no schemas provided)"
}
]
}
end
+
+ test "Custom Header params" do
+ conn =
+ :post
+ |> Plug.Test.conn("/api/pets/1/adopt")
+ |> Plug.Conn.put_req_header("content-type", "application/json; charset=UTF-8")
+ |> Plug.Conn.put_req_header("x-user-id", "123456")
+ |> OpenApiSpexTest.Router.call([])
+
+ assert Jason.decode!(conn.resp_body) == %{
+ "data" => %{
+ "pet_type" => "Dog",
+ "bark" => "woof"
+ }
+ }
+ end
end
end
diff --git a/test/support/pet_controller.ex b/test/support/pet_controller.ex
index f6991ac..66d6cd4 100644
--- a/test/support/pet_controller.ex
+++ b/test/support/pet_controller.ex
@@ -1,90 +1,123 @@
defmodule OpenApiSpexTest.PetController do
use Phoenix.Controller
alias OpenApiSpex.Operation
alias OpenApiSpexTest.Schemas
plug OpenApiSpex.Plug.CastAndValidate
def open_api_operation(action) do
apply(__MODULE__, :"#{action}_operation", [])
end
@doc """
API Spec for :show action
"""
def show_operation() do
import Operation
%Operation{
tags: ["pets"],
summary: "Show pet",
description: "Show a pet by ID",
operationId: "PetController.show",
parameters: [
parameter(:id, :path, :integer, "Pet ID", example: 123, minimum: 1)
],
responses: %{
200 => response("Pet", "application/json", Schemas.PetResponse)
}
}
end
def show(conn, %{id: _id}) do
json(conn, %Schemas.PetResponse{
data: %Schemas.Dog{
pet_type: "Dog",
bark: "woof"
}
})
end
def index_operation() do
import Operation
%Operation{
tags: ["pets"],
summary: "List pets",
description: "List all petes",
operationId: "PetController.index",
parameters: [
parameter(:validParam, :query, :boolean, "Valid Param", example: true)
],
responses: %{
200 => response("Pet List Response", "application/json", Schemas.PetsResponse)
}
}
end
def index(conn, _params) do
json(conn, %Schemas.PetsResponse{
data: [
%Schemas.Dog{
pet_type: "Dog",
bark: "joe@gmail.com"
}
]
})
end
def create_operation() do
import Operation
%Operation{
tags: ["pets"],
summary: "Create pet",
description: "Create a pet",
operationId: "PetController.create",
parameters: [],
requestBody: request_body("The pet attributes", "application/json", Schemas.PetRequest),
responses: %{
201 => response("Pet", "application/json", Schemas.PetRequest)
}
}
end
def create(conn = %{body_params: %Schemas.PetRequest{pet: pet}}, _) do
json(conn, %Schemas.PetResponse{
data: pet
})
end
+
+ def adopt_operation() do
+ import Operation
+
+ %Operation{
+ tags: ["pets"],
+ summary: "Adopt pet",
+ description: "Adopt a pet",
+ operationId: "PetController.adopt",
+ parameters: [
+ parameter(
+ "x-user-id",
+ :header,
+ :string,
+ "User that performs this action",
+ required: true
+ ),
+ parameter(:id, :path, :integer, "Pet ID", example: 123, minimum: 1)
+ ],
+ responses: %{
+ 200 => response("Pet", "application/json", Schemas.PetRequest)
+ }
+ }
+ end
+
+ def adopt(conn, _) do
+ json(conn, %Schemas.PetResponse{
+ data: %Schemas.Dog{
+ pet_type: "Dog",
+ bark: "woof"
+ }
+ })
+ end
end
diff --git a/test/support/router.ex b/test/support/router.ex
index 603838e..20db060 100644
--- a/test/support/router.ex
+++ b/test/support/router.ex
@@ -1,26 +1,27 @@
defmodule OpenApiSpexTest.Router do
use Phoenix.Router
alias Plug.Parsers
alias OpenApiSpex.Plug.{PutApiSpec, RenderSpec}
pipeline :api do
plug :accepts, ["json"]
plug PutApiSpec, module: OpenApiSpexTest.ApiSpec
plug Parsers, parsers: [:json], pass: ["text/*"], json_decoder: Jason
end
scope "/api", OpenApiSpexTest do
pipe_through :api
resources "/users", UserController, only: [:create, :index, :show]
# Used by ParamsTest
resources "/custom_error_users", CustomErrorUserController, only: [:index]
get "/users/:id/payment_details", UserController, :payment_details
post "/users/:id/contact_info", UserController, :contact_info
post "/users/create_entity", UserController, :create_entity
get "/openapi", RenderSpec, []
resources "/pets", PetController, only: [:create, :index, :show]
+ post "/pets/:id/adopt", PetController, :adopt
end
end

File Metadata

Mime Type
text/x-diff
Expires
Tue, Nov 26, 7:41 PM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40466
Default Alt Text
(14 KB)

Event Timeline