Page MenuHomePhorge

No OneTemporary

Size
13 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 0242981..efc29ed 100644
--- a/lib/open_api_spex/cast_parameters.ex
+++ b/lib/open_api_spex/cast_parameters.ex
@@ -1,97 +1,110 @@
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
# 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))
ctx = %Cast{
value: params,
schema: location_schema,
schemas: components.schemas
}
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, _ -> {: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
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 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,
additionalProperties: false,
properties: %{},
required: []
}
add_to_location_schema(template_schema, parameter)
end
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
properties = Map.put(location_schema.properties, parameter.name, Parameter.schema(parameter))
+ location_schema =
+ maybe_add_additional_properties(location_schema, parameter.schema)
+
%{location_schema | properties: properties, required: required}
end
+
+ defp maybe_add_additional_properties(
+ %Schema{additionalProperties: false} = location_schema,
+ %Schema{type: :object, additionalProperties: ap}
+ )
+ when ap != false and not is_nil(ap) do
+ %{location_schema | additionalProperties: ap}
+ end
+
+ defp maybe_add_additional_properties(location_schema, _param_schema), do: location_schema
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/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..941896b
--- /dev/null
+++ b/test/support/utility_controller.ex
@@ -0,0 +1,32 @@
+defmodule OpenApiSpexTest.UtilityController do
+ use Phoenix.Controller
+ alias OpenApiSpex.Operation
+ alias OpenApiSpexTest.Schemas
+ 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, 4:40 PM (1 d, 9 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40431
Default Alt Text
(13 KB)

Event Timeline