Skip to content

Commit f14596b

Browse files
committed
Add tests and fix parameter exploding issue
1 parent 7c05fa0 commit f14596b

File tree

7 files changed

+198
-4
lines changed

7 files changed

+198
-4
lines changed

examples/plug_app/lib/plug_app/user_handler.ex

+31-2
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,31 @@ defmodule PlugApp.UserHandler do
5858
description: "Show a user by ID",
5959
operationId: "UserHandler.Show",
6060
parameters: [
61-
parameter(:id, :path, %Schema{type: :integer, minimum: 1}, "User ID", example: 123)
61+
parameter(:id, :path, %Schema{type: :integer, minimum: 1}, "User ID", example: 123),
62+
parameter(:qux, :query, %Schema{type: :string}, "qux param", required: false),
63+
parameter(
64+
:some,
65+
:query,
66+
%Schema{
67+
type: :object,
68+
oneOf: [
69+
%Schema{
70+
type: :object,
71+
properties: %{foo: %Schema{type: :string}, bar: %Schema{type: :string}},
72+
required: [:foo]
73+
},
74+
%Schema{
75+
type: :object,
76+
properties: %{foo: %Schema{type: :string}, baz: %Schema{type: :string}},
77+
required: [:baz]
78+
}
79+
]
80+
},
81+
"Some query parameter ",
82+
explode: true,
83+
style: :form,
84+
required: true
85+
)
6286
],
6387
responses: %{
6488
200 => response("User", "application/json", Schemas.UserResponse)
@@ -79,7 +103,12 @@ defmodule PlugApp.UserHandler do
79103
end
80104
end
81105

82-
def show(conn = %Plug.Conn{assigns: %{user: user}}, _opts) do
106+
# def show(conn = %Plug.Conn{assigns: %{user: user}}, _opts) do
107+
def show(conn, _opts) do
108+
IO.inspect(conn.params)
109+
110+
user = Accounts.get_user!(conn.params.id)
111+
83112
conn
84113
|> put_resp_header("content-type", "application/json")
85114
|> send_resp(200, render(user))

examples/plug_app/mix.exs

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ defmodule PlugApp.Mixfile do
2929
{:plug, "~> 1.0"},
3030
{:ecto, "~> 2.2"},
3131
{:sqlite_ecto2, "~> 2.4"},
32-
{:jason, "~> 1.0"}
32+
{:jason, "~> 1.0"},
33+
{:cors_plug, "~> 3.0"}
3334
# {:dep_from_hexpm, "~> 0.3.0"},
3435
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
3536
]

examples/plug_app/mix.lock

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
%{
22
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
3+
"cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"},
34
"cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
45
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
56
"cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},

examples/plug_app/test/user_handler_test.exs

+20-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ defmodule UserHandlerTest do
4646
} do
4747
%{resp_body: body} =
4848
conn =
49-
conn(:get, "/api/users/#{user_id}")
49+
conn(:get, "/api/users/#{user_id}?foo=asd")
5050
|> Router.call(@opts)
5151

5252
assert %{status: 200} = conn
@@ -57,6 +57,25 @@ defmodule UserHandlerTest do
5757

5858
assert_schema(json_response, "UserResponse", api_spec)
5959
end
60+
61+
test "responds with 422 when there is no either foo nor bar in query params", %{
62+
user: %{id: user_id},
63+
api_spec: _api_spec
64+
} do
65+
%{resp_body: body} =
66+
conn =
67+
conn(:get, "/api/users/#{user_id}")
68+
|> Router.call(@opts)
69+
70+
assert %{status: 422} = conn
71+
72+
json_response = Jason.decode!(body)
73+
74+
IO.inspect(json_response)
75+
# assert %{} = json_response
76+
77+
# assert_schema(json_response, "UserResponse", api_spec)
78+
end
6079
end
6180

6281
describe "POST /api/users" do

lib/open_api_spex/cast_parameters.ex

+58
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,13 @@ defmodule OpenApiSpex.CastParameters do
6969
properties: parameters |> Map.new(fn p -> {p.name, Parameter.schema(p)} end),
7070
required: parameters |> Enum.filter(& &1.required) |> Enum.map(& &1.name)
7171
}
72+
# |> maybe_combine_oneOfs(parameters, components)
7273
|> maybe_add_additional_properties(components),
7374
parameters_contexts(parameters)
7475
}
7576
end
7677

78+
7779
# Extract context information from parameters, useful later when casting
7880
defp parameters_contexts(parameters) do
7981
Map.new(parameters, fn parameter ->
@@ -127,13 +129,53 @@ defmodule OpenApiSpex.CastParameters do
127129
location,
128130
schema.properties |> Map.keys() |> Enum.map(&Atom.to_string/1)
129131
)
132+
|> maybe_combine_params(schema, parameters_contexts)
130133
|> pre_parse_parameters(parameters_contexts, parsers)
131134
|> case do
132135
{:error, _} = err -> err
133136
params -> Cast.cast(schema, params, components.schemas, opts)
134137
end
135138
end
136139

140+
# in caase some parameters have explode: true we want to search for those
141+
# fields in parameters and combine the parameters in a single struct
142+
# so that the casting can do further work
143+
defp maybe_combine_params(%{} = parameters, %{} = schema, %{} = parameters_contexts) do
144+
Enum.reduce(parameters_contexts, parameters, fn
145+
{key, %{explode: true}}, parameters ->
146+
# we have exploding property, we need to search for it's possible fields
147+
# and add them under the key into the parameters struct.
148+
# do we leave the fields in the params as well? not sure.
149+
schema_of_exploding_property = Map.get(schema.properties, String.to_existing_atom(key), %{})
150+
151+
fields =
152+
Schema.properties(schema_of_exploding_property) ++
153+
Schema.possible_properties(schema_of_exploding_property)
154+
155+
{struct_params, found_keys} =
156+
Enum.reduce(fields, {Map.new(), []}, fn {field_key, _}, {struct_params, found_keys} ->
157+
param_field_key = field_key |> Atom.to_string()
158+
val = Map.get(parameters, param_field_key)
159+
160+
{new_params, new_found_keys} =
161+
unless is_nil(val) do
162+
{Map.put(struct_params, param_field_key, val), [param_field_key | found_keys]}
163+
else
164+
{struct_params, found_keys}
165+
end
166+
167+
{new_params, new_found_keys}
168+
end)
169+
170+
parameters
171+
|> Map.drop(found_keys)
172+
|> Map.put(key, struct_params)
173+
174+
_, parameters ->
175+
parameters
176+
end)
177+
end
178+
137179
defp pre_parse_parameters(%{} = parameters, %{} = parameters_context, parsers) do
138180
Enum.reduce_while(parameters, Map.new(), fn {key, value}, acc ->
139181
case pre_parse_parameter(value, Map.get(parameters_context, key, %{}), parsers) do
@@ -208,4 +250,20 @@ defmodule OpenApiSpex.CastParameters do
208250
_ -> schema
209251
end
210252
end
253+
254+
defp maybe_combine_oneOfs(schema, parameters, components) do
255+
# check if any params have explode,
256+
# if so add the properties of it's schema to the top level
257+
# and remove the key for that
258+
%{}
259+
end
260+
261+
defp create_one_of_schemas(parameters) do
262+
if Enum.any?(parameters, fn p ->
263+
p.explode == true and is_list(Parameter.schema(p).oneOf)
264+
end) do
265+
# in this case we need to create multiple schemas. Each of the schemas
266+
# has to have properties defined in other parameters + add required properties
267+
end
268+
end
211269
end

lib/open_api_spex/schema.ex

+17
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,23 @@ defmodule OpenApiSpex.Schema do
344344

345345
def properties(_), do: []
346346

347+
@doc """
348+
Get the names of all properties possible for polymorphic schemas using `oneOf`.
349+
350+
This is different from properties/1 in that it returns properties that *might*
351+
be a part of the schema sometimes based on the discriminator.
352+
"""
353+
354+
def possible_properties(%Schema{oneOf: schemas}) when is_list(schemas) do
355+
Enum.flat_map(schemas, &properties/1) |> Enum.uniq()
356+
end
357+
358+
def possible_properties(%Schema{anyOf: schemas}) when is_list(schemas) do
359+
Enum.flat_map(schemas, &properties/1) |> Enum.uniq()
360+
end
361+
362+
def possible_properties(_), do: []
363+
347364
@doc """
348365
Generate example value from a `%Schema{}` struct.
349366

test/cast_parameters_test.exs

+69
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
defmodule OpenApiSpex.CastParametersTest do
22
use ExUnit.Case
33

4+
require IEx
5+
46
alias OpenApiSpex.{
57
CastParameters,
68
Components,
@@ -191,6 +193,23 @@ defmodule OpenApiSpex.CastParametersTest do
191193
[%OpenApiSpex.Cast.Error{format: "application/json", reason: :invalid_format}]} =
192194
CastParameters.cast(conn, operation, spec)
193195
end
196+
197+
test "cast param with oneof and valid args" do
198+
{operation, spec} = oneof_query_spec_operation()
199+
200+
filter_params = URI.encode_query(%{size: "XL"})
201+
202+
conn =
203+
:get
204+
|> Plug.Test.conn("/api/users?#{filter_params}")
205+
|> Plug.Conn.put_req_header("content-type", "application/json")
206+
|> Plug.Conn.fetch_query_params()
207+
208+
# require IEx
209+
# IEx.pry()
210+
211+
assert {:ok, _} = CastParameters.cast(conn, operation, spec)
212+
end
194213
end
195214

196215
defp create_conn() do
@@ -294,4 +313,54 @@ defmodule OpenApiSpex.CastParametersTest do
294313

295314
{operation, spec}
296315
end
316+
317+
defp oneof_query_spec_operation() do
318+
schema = %Schema{
319+
type: :object,
320+
title: "Filters",
321+
oneOf: [
322+
%Schema{
323+
type: :object,
324+
properties: %{
325+
size: %Schema{type: :string, pattern: "^XS|S|M|L|XL$"},
326+
color: %Schema{type: :string}
327+
},
328+
required: [:size]
329+
},
330+
%Schema{
331+
type: :object,
332+
properties: %{
333+
size: %Schema{type: :string, pattern: "^XS|S|M|L|XL$"},
334+
color: %Schema{type: :string}
335+
},
336+
required: [:color]
337+
}
338+
],
339+
example: %{size: "XL"}
340+
}
341+
342+
parameter = %Parameter{
343+
in: :query,
344+
name: :filter,
345+
required: false,
346+
schema: %Reference{"$ref": "#/components/schemas/Filters"},
347+
explode: true,
348+
style: :form,
349+
required: true
350+
}
351+
352+
operation = %Operation{
353+
parameters: [parameter],
354+
responses: %{
355+
200 => %Schema{type: :object}
356+
}
357+
}
358+
359+
spec =
360+
spec_with_components(%Components{
361+
schemas: %{"Filters" => schema}
362+
})
363+
364+
{operation, spec}
365+
end
297366
end

0 commit comments

Comments
 (0)