Page MenuHomePhorge

No OneTemporary

Size
17 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 fe9f682..cc84246 100644
--- a/lib/open_api_spex/cast_parameters.ex
+++ b/lib/open_api_spex/cast_parameters.ex
@@ -1,82 +1,104 @@
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
- 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
+ with {:ok, params} <- cast_to_params(conn, operation, components) do
+ {:ok, %{conn | params: params}}
end
end
+ defp cast_to_params(conn, operation, components) do
+ operation
+ |> schemas_by_location(components)
+ |> Enum.map(fn {location, schema} -> cast_location(location, schema, components, conn) end)
+ |> reduce_cast_results()
+ end
+
defp get_params_by_location(conn, :query, _) do
Plug.Conn.fetch_query_params(conn).query_params
end
defp get_params_by_location(conn, :path, _) do
conn.path_params
end
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
defp create_location_schema(parameters) do
%Schema{
type: :object,
additionalProperties: false,
- properties: Map.new(parameters, fn p -> {p.name, Parameter.schema(p)} end),
+ properties: parameters |> Map.new(fn p -> {p.name, Parameter.schema(p)} end),
required: parameters |> Enum.filter(& &1.required) |> Enum.map(& &1.name)
}
+ |> maybe_add_additional_properties()
end
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)
Map.new(param_specs_by_location, fn {location, parameters} ->
{location, create_location_schema(parameters)}
end)
end
defp cast_location(location, schema, components, conn) do
- params = get_params_by_location(conn, location, Map.keys(schema.properties))
+ params =
+ get_params_by_location(
+ conn,
+ location,
+ schema.properties |> Map.keys() |> Enum.map(&Atom.to_string/1)
+ )
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
+
+ defp maybe_add_additional_properties(schema) do
+ ap_schema =
+ Enum.reject(
+ schema.properties,
+ fn {_name, %{additionalProperties: ap}} ->
+ is_nil(ap) or ap == false
+ end
+ )
+
+ case ap_schema do
+ [{_, %{additionalProperties: ap}}] -> %{schema | additionalProperties: ap}
+ _ -> schema
+ end
+ end
end
diff --git a/test/plug/cast_test.exs b/test/plug/cast_test.exs
index 5930d6b..72a7824 100644
--- a/test/plug/cast_test.exs
+++ b/test/plug/cast_test.exs
@@ -1,254 +1,268 @@
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
+
+ test "freeForm params" do
+ conn =
+ :get
+ |> Plug.Test.conn("/api/utility/echo/any?one=this&two=cam&three=be&anything=true")
+ |> OpenApiSpexTest.Router.call([])
+
+ assert Jason.decode!(conn.resp_body) == %{
+ "one" => "this",
+ "two" => "cam",
+ "three" => "be",
+ "anything" => "true"
+ }
+ end
end
end
diff --git a/test/support/pet_controller.ex b/test/support/pet_controller.ex
index a31132f..0707547 100644
--- a/test/support/pet_controller.ex
+++ b/test/support/pet_controller.ex
@@ -1,128 +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(:"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
+ 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
+ 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 20db060..d6528cf 100644
--- a/test/support/router.ex
+++ b/test/support/router.ex
@@ -1,27 +1,29 @@
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
+
+ get "/utility/echo/any", UtilityController, :echo_any
end
end
diff --git a/test/support/utility_controller.ex b/test/support/utility_controller.ex
new file mode 100644
index 0000000..6117001
--- /dev/null
+++ b/test/support/utility_controller.ex
@@ -0,0 +1,31 @@
+defmodule OpenApiSpexTest.UtilityController do
+ use Phoenix.Controller
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+
+ plug OpenApiSpex.Plug.CastAndValidate
+
+ def open_api_operation(action) do
+ apply(__MODULE__, :"#{action}_operation", [])
+ end
+
+ def echo_any_operation() do
+ import Operation
+
+ %Operation{
+ tags: ["utility"],
+ summary: "Echo parameters",
+ operationId: "UtilityController.echo",
+ parameters: [
+ parameter(:freeForm, :query, %Schema{type: :object, additionalProperties: true}, "unspecified list of anything")
+ ],
+ responses: %{
+ 200 => response("Casted Result", "application/json", %Schema{type: :object, additionalProperties: true})
+ }
+ }
+ end
+
+ def echo_any(conn, params) do
+ json(conn, params)
+ end
+end

File Metadata

Mime Type
text/x-diff
Expires
Tue, Nov 26, 2:34 PM (1 d, 10 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40386
Default Alt Text
(17 KB)

Event Timeline