Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F114762
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
17 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Tue, Nov 26, 2:34 PM (1 d, 13 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40386
Default Alt Text
(17 KB)
Attached To
Mode
R22 open_api_spex
Attached
Detach File
Event Timeline
Log In to Comment