Page MenuHomePhorge

No OneTemporary

Size
15 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..fe9f682 100644
--- a/lib/open_api_spex/cast_parameters.ex
+++ b/lib/open_api_spex/cast_parameters.ex
@@ -1,45 +1,82 @@
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
- end)
+ full_cast_result =
+ schemas_by_location(operation, components)
+ |> Enum.map(fn {location, schema} -> cast_location(location, schema, components, conn) end)
+ |> reduce_cast_results()
+
+ case full_cast_result do
+ {:ok, params} -> {:ok, %{conn | params: params}}
+ err -> err
+ end
+ end
+
+ defp get_params_by_location(conn, :query, _) do
+ Plug.Conn.fetch_query_params(conn).query_params
+ end
- properties =
- resolved_parameters
- |> Enum.map(fn parameter -> {parameter.name, Parameter.schema(parameter)} end)
- |> Map.new()
+ defp get_params_by_location(conn, :path, _) do
+ conn.path_params
+ end
- required =
- resolved_parameters
- |> Enum.filter(& &1.required)
- |> Enum.map(& &1.name)
+ defp get_params_by_location(conn, :cookie, _) do
+ Plug.Conn.fetch_cookies(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
- object_schema = %Schema{
+ defp create_location_schema(parameters) do
+ %Schema{
type: :object,
- properties: properties,
- required: required,
- additionalProperties: false
+ additionalProperties: false,
+ properties: Map.new(parameters, fn p -> {p.name, Parameter.schema(p)} end),
+ required: parameters |> Enum.filter(& &1.required) |> Enum.map(& &1.name)
}
+ end
- params = Map.merge(conn.path_params, conn.query_params)
+ defp schemas_by_location(operation, components) do
+ param_specs_by_location =
+ operation.parameters
+ |> Enum.map(fn
+ %Reference{} = ref -> Reference.resolve_parameter(ref, components.parameters)
+ %Parameter{} = param -> param
+ end)
+ |> Enum.group_by(& &1.in)
- ctx = %Cast{value: params, schema: object_schema, schemas: components.schemas}
+ Map.new(param_specs_by_location, fn {location, parameters} ->
+ {location, create_location_schema(parameters)}
+ end)
+ end
- with {:ok, params} <- Object.cast(ctx) do
- {:ok, %{conn | params: params}}
- end
+ defp cast_location(location, schema, components, conn) do
+ params = get_params_by_location(conn, location, Map.keys(schema.properties))
+
+ ctx = %Cast{
+ value: params,
+ schema: schema,
+ schemas: components.schemas
+ }
+
+ Object.cast(ctx)
+ end
+
+ defp reduce_cast_results(results) do
+ Enum.reduce_while(results, {:ok, %{}}, fn
+ {:ok, params}, {:ok, all_params} -> {:cont, {:ok, Map.merge(all_params, params)}}
+ cast_error, _ -> {:halt, cast_error}
+ end)
end
end
diff --git a/test/plug/cast_test.exs b/test/plug/cast_test.exs
index 07a544a..5930d6b 100644
--- a/test/plug/cast_test.exs
+++ b/test/plug/cast_test.exs
@@ -1,222 +1,254 @@
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 "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
+ test "Cookie 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")
+ |> Plug.Conn.put_req_header("cookie", "debug=1")
+ |> OpenApiSpexTest.Router.call([])
+
+ assert Jason.decode!(conn.resp_body) == %{
+ "data" => %{
+ "pet_type" => "Debug-Dog",
+ "bark" => "woof"
+ }
+ }
+ end
end
end
diff --git a/test/support/pet_controller.ex b/test/support/pet_controller.ex
index f6991ac..a31132f 100644
--- a/test/support/pet_controller.ex
+++ b/test/support/pet_controller.ex
@@ -1,90 +1,128 @@
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),
+ parameter(:status, :query, :string, "New status"),
+ parameter(:debug, :cookie, %OpenApiSpex.Schema{type: :integer, enum: [0, 1], default: 0}, "Debug"),
+ ],
+ responses: %{
+ 200 => response("Pet", "application/json", Schemas.PetRequest)
+ }
+ }
+ end
+
+ def adopt(conn, %{"x-user-id" => _user_id, :id => _id, :debug => 0}) do
+ json(conn, %Schemas.PetResponse{
+ data: %Schemas.Dog{
+ pet_type: "Dog",
+ bark: "woof"
+ }
+ })
+ end
+
+ def adopt(conn, %{"x-user-id" => _user_id, :id => _id, :debug => 1}) do
+ json(conn, %Schemas.PetResponse{
+ data: %Schemas.Dog{
+ pet_type: "Debug-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, 5:25 PM (1 d, 9 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40442
Default Alt Text
(15 KB)

Event Timeline