Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F114724
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
12 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/open_api_spex/cast/discriminator.ex b/lib/open_api_spex/cast/discriminator.ex
index 8945d76..203f9c0 100644
--- a/lib/open_api_spex/cast/discriminator.ex
+++ b/lib/open_api_spex/cast/discriminator.ex
@@ -1,112 +1,112 @@
defmodule OpenApiSpex.Cast.Discriminator do
@moduledoc """
Defines the `OpenApiSpex.Discriminator.t` type.
"""
alias OpenApiSpex.{Cast, Reference, Schema}
@enforce_keys :propertyName
defstruct [
:propertyName,
:mapping
]
@typedoc """
[Discriminator Object](https://swagger.io/specification/#discriminatorObject)
When request bodies or response payloads may be one of a number of different schemas,
a discriminator object can be used to aid in serialization, deserialization, and validation.
The discriminator is a specific object in a schema which is used to inform the consumer of the
specification of an alternative schema based on the value associated with it.
A discriminator requires a composite key be set on the schema:
* `allOf`
* `oneOf`
* `anyOf`
"""
@type t :: %__MODULE__{
propertyName: String.t(),
mapping: %{String.t() => String.t()} | nil
}
def cast(ctx) do
case cast_discriminator(ctx) do
{:ok, result} -> {:ok, result}
error -> error
end
end
defp cast_discriminator(%_{value: value, schema: schema} = ctx) do
{discriminator_property, mappings} = discriminator_details(schema)
case Map.pop(value, "#{discriminator_property}") do
{"", _} ->
error(:no_value_for_discriminator, ctx)
{discriminator_value, _castable_value} ->
# The cast specified by the composite key (allOf, anyOf, oneOf) MUST succeed
# or return an error according to the Open API Spec.
composite_ctx = %{
ctx
| schema: %{schema | discriminator: nil},
path: ["#{discriminator_property}" | ctx.path]
}
cast_composition(composite_ctx, ctx, discriminator_value, mappings)
end
end
defp cast_composition(composite_ctx, ctx, discriminator_value, mappings) do
with {composite_schemas, {:ok, _}} <- cast_composition(composite_ctx),
%{} = schema <-
find_discriminator_schema(discriminator_value, mappings, composite_schemas) do
Cast.cast(%{composite_ctx | schema: schema})
else
nil -> error(:invalid_discriminator_value, ctx)
other -> other
end
end
defp cast_composition(%_{schema: %{anyOf: schemas, discriminator: nil}} = ctx)
when is_list(schemas),
- do: {schemas |> _locate_schemas(ctx.schemas), Cast.cast(ctx)}
+ do: {locate_schemas(schemas, ctx.schemas), Cast.cast(ctx)}
defp cast_composition(%_{schema: %{allOf: schemas, discriminator: nil}} = ctx)
when is_list(schemas),
- do: {schemas |> _locate_schemas(ctx.schemas), Cast.cast(ctx)}
+ do: {locate_schemas(schemas, ctx.schemas), Cast.cast(ctx)}
defp cast_composition(%_{schema: %{oneOf: schemas, discriminator: nil}} = ctx)
when is_list(schemas),
- do: {schemas |> _locate_schemas(ctx.schemas), Cast.cast(ctx)}
+ do: {locate_schemas(schemas, ctx.schemas), Cast.cast(ctx)}
defp find_discriminator_schema(discriminator, mappings = %{}, schemas) do
with {:ok, "#/components/schemas/" <> name} <- Map.fetch(mappings, discriminator) do
find_discriminator_schema(name, nil, schemas)
else
{:ok, name} -> find_discriminator_schema(name, nil, schemas)
:error -> find_discriminator_schema(discriminator, nil, schemas)
end
end
defp find_discriminator_schema(discriminator, _, schemas) do
Enum.find(schemas, &Kernel.==(&1.title, discriminator))
end
defp discriminator_details(%{discriminator: %{propertyName: property_name, mapping: mappings}}),
do: {String.to_existing_atom(property_name), mappings}
defp error(message, %{schema: %{discriminator: %{propertyName: property}}} = ctx) do
Cast.error(ctx, {message, property})
end
- defp _locate_schemas(schemas, ctx_schemas) do
+ defp locate_schemas(schemas, ctx_schemas) do
schemas
|> Enum.map(fn
%Schema{} = schema ->
schema
%Reference{} = schema ->
Reference.resolve_schema(schema, ctx_schemas)
end)
end
end
diff --git a/test/plug/cast_test.exs b/test/plug/cast_test.exs
index ab978ac..f3f05fe 100644
--- a/test/plug/cast_test.exs
+++ b/test/plug/cast_test.exs
@@ -1,302 +1,302 @@
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 "Optional param" do
conn =
:post
|> Plug.Test.conn("/api/pets/1/adopt?status=adopted")
|> Plug.Conn.put_req_header("content-type", "application/json; charset=UTF-8")
- |> Plug.Conn.put_req_header("x-user-id", "123456")
- |> OpenApiSpexTest.Router.call([])
+ |> Plug.Conn.put_req_header("x-user-id", "123456")
+ |> OpenApiSpexTest.Router.call([])
assert Jason.decode!(conn.resp_body) == %{
"data" => %{
- "pet_type" => "Dog",
- "bark" => "woof"
- }
+ "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 "Discriminator with mapping" do
body =
Jason.encode!(%{
appointment_type: "grooming",
hair_trim: true,
nail_clip: false
})
conn =
:post
|> Plug.Test.conn("/api/pets/appointment", body)
|> Plug.Conn.put_req_header("content-type", "application/json")
|> OpenApiSpexTest.Router.call([])
assert Jason.decode!(conn.resp_body) == %{"data" => [%{"pet_type" => "Dog", "bark" => "bow wow"}]}
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
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Tue, Nov 26, 1:45 PM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40350
Default Alt Text
(12 KB)
Attached To
Mode
R22 open_api_spex
Attached
Detach File
Event Timeline
Log In to Comment