Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F115238
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
33 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/open_api_spex/cast.ex b/lib/open_api_spex/cast.ex
index f9b155e..8d059c8 100644
--- a/lib/open_api_spex/cast.ex
+++ b/lib/open_api_spex/cast.ex
@@ -1,189 +1,190 @@
defmodule OpenApiSpex.Cast do
alias OpenApiSpex.{Reference, Schema}
alias OpenApiSpex.Reference
alias OpenApiSpex.Cast.{
AllOf,
AnyOf,
Array,
Discriminator,
Error,
Integer,
Object,
OneOf,
Primitive,
String
}
@type schema_or_reference :: Schema.t() | Reference.t()
@type t :: %__MODULE__{
value: term(),
schema: schema_or_reference | nil,
schemas: map(),
path: [atom() | String.t() | integer()],
key: atom() | nil,
index: integer,
errors: [Error.t()]
}
defstruct value: nil,
schema: nil,
schemas: %{},
path: [],
key: nil,
index: 0,
errors: []
@doc ~S"""
Cast and validate a value against the given schema.
Recognizes all the types defined in Open API (itself a superset of JSON Schema).
JSON Schema types:
[https://json-schema.org/latest/json-schema-core.html#rfc.section.4.2.1](https://json-schema.org/latest/json-schema-core.html#rfc.section.4.2.1)
Open API primitive types:
[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#data-types](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#data-types)
For an `:object` schema type, the cast operation returns a map with atom keys.
## Examples
iex> alias OpenApiSpex.{Cast, Schema}
iex> schema = %Schema{type: :string}
iex> Cast.cast(schema, "a string")
{:ok, "a string"}
iex> Cast.cast(schema, :not_a_string)
{
:error,
[
%OpenApiSpex.Cast.Error{
reason: :invalid_type,
type: :string,
value: :not_a_string
}
]
}
iex> schema = %Schema{
...> type: :object,
...> properties: %{
...> name: nil
- ...> }
+ ...> },
+ ...> additionalProperties: false
...> }
iex> Cast.cast(schema, %{"name" => "spex"})
{:ok, %{name: "spex"}}
iex> Cast.cast(schema, %{"bad" => "spex"})
{
:error,
[
%OpenApiSpex.Cast.Error{
name: "bad",
path: ["bad"],
reason: :unexpected_field,
value: %{"bad" => "spex"}
}
]
}
"""
@spec cast(schema_or_reference | nil, term(), map()) :: {:ok, term()} | {:error, [Error.t()]}
def cast(schema, value, schemas \\ %{}) do
ctx = %__MODULE__{schema: schema, value: value, schemas: schemas}
cast(ctx)
end
@spec cast(t()) :: {:ok, term()} | {:error, [Error.t()]}
# nil schema
def cast(%__MODULE__{value: value, schema: nil}),
do: {:ok, value}
def cast(%__MODULE__{schema: %Reference{}} = ctx) do
schema = Reference.resolve_schema(ctx.schema, ctx.schemas)
cast(%{ctx | schema: schema})
end
# nullable: true
def cast(%__MODULE__{value: nil, schema: %{nullable: true}}) do
{:ok, nil}
end
# nullable: false
def cast(%__MODULE__{value: nil} = ctx) do
error(ctx, {:null_value})
end
# Enum
def cast(%__MODULE__{schema: %{enum: []}} = ctx) do
cast(%{ctx | schema: %{ctx.schema | enum: nil}})
end
# Enum
def cast(%__MODULE__{schema: %{enum: enum}} = ctx) when is_list(enum) do
with {:ok, value} <- cast(%{ctx | schema: %{ctx.schema | enum: nil}}) do
OpenApiSpex.Cast.Enum.cast(%{ctx | value: value})
end
end
## Specific types
def cast(%__MODULE__{schema: %{type: :object, discriminator: discriminator}} = ctx)
when is_map(discriminator),
do: Discriminator.cast(ctx)
def cast(%__MODULE__{schema: %{type: _, anyOf: schemas}} = ctx) when is_list(schemas),
do: AnyOf.cast(ctx)
def cast(%__MODULE__{schema: %{type: _, allOf: schemas}} = ctx) when is_list(schemas),
do: AllOf.cast(ctx)
def cast(%__MODULE__{schema: %{type: _, oneOf: schemas}} = ctx) when is_list(schemas),
do: OneOf.cast(ctx)
def cast(%__MODULE__{schema: %{type: :object}} = ctx),
do: Object.cast(ctx)
def cast(%__MODULE__{schema: %{type: :boolean}} = ctx),
do: Primitive.cast_boolean(ctx)
def cast(%__MODULE__{schema: %{type: :integer}} = ctx),
do: Integer.cast(ctx)
def cast(%__MODULE__{schema: %{type: :number}} = ctx),
do: Primitive.cast_number(ctx)
def cast(%__MODULE__{schema: %{type: :string}} = ctx),
do: String.cast(ctx)
def cast(%__MODULE__{schema: %{type: :array}} = ctx),
do: Array.cast(ctx)
def cast(%__MODULE__{schema: %{type: _other}} = ctx),
do: error(ctx, {:invalid_schema_type})
def cast(%{} = ctx), do: cast(struct(__MODULE__, ctx))
def cast(ctx) when is_list(ctx), do: cast(struct(__MODULE__, ctx))
# Add an error
def error(ctx, error_args) do
error = Error.new(ctx, error_args)
{:error, [error | ctx.errors]}
end
def ok(%__MODULE__{value: value}), do: {:ok, value}
def success(%__MODULE__{schema: schema} = ctx, schema_properties)
when is_list(schema_properties) do
schema_without_successful_validation_property =
Enum.reduce(schema_properties, schema, fn property, schema ->
%{schema | property => nil}
end)
{:cast, %{ctx | schema: schema_without_successful_validation_property}}
end
def success(%__MODULE__{schema: _schema} = ctx, schema_property) do
success(ctx, [schema_property])
end
end
diff --git a/lib/open_api_spex/cast/object.ex b/lib/open_api_spex/cast/object.ex
index a87c067..e2f1aac 100644
--- a/lib/open_api_spex/cast/object.ex
+++ b/lib/open_api_spex/cast/object.ex
@@ -1,145 +1,174 @@
defmodule OpenApiSpex.Cast.Object do
@moduledoc false
alias OpenApiSpex.Cast
alias OpenApiSpex.Cast.Error
def cast(%{value: value} = ctx) when not is_map(value) do
Cast.error(ctx, {:invalid_type, :object})
end
def cast(%{value: value, schema: %{properties: nil}}) do
{:ok, value}
end
def cast(%{value: value, schema: schema} = ctx) do
+ original_value = value
schema_properties = schema.properties || %{}
with :ok <- check_unrecognized_properties(ctx, schema_properties),
value = cast_atom_keys(value, schema_properties),
ctx = %{ctx | value: value},
+ ctx = cast_additional_properties(ctx, original_value),
:ok <- check_required_fields(ctx, schema),
:ok <- check_max_properties(ctx),
:ok <- check_min_properties(ctx),
{:ok, value} <- cast_properties(%{ctx | schema: schema_properties}) do
value_with_defaults = apply_defaults(value, schema_properties)
ctx = to_struct(%{ctx | value: value_with_defaults})
{:ok, ctx}
end
end
# When additionalProperties is true, extra properties are allowed in input
- defp check_unrecognized_properties(%{schema: %{additionalProperties: true}}, _expected_keys) do
+ defp check_unrecognized_properties(%{schema: %{additionalProperties: ap}}, _expected_keys)
+ when ap in [nil, true] do
:ok
end
defp check_unrecognized_properties(%{value: value} = ctx, expected_keys) do
input_keys = value |> Map.keys() |> Enum.map(&to_string/1)
schema_keys = expected_keys |> Map.keys() |> Enum.map(&to_string/1)
extra_keys = input_keys -- schema_keys
if extra_keys == [] do
:ok
else
[name | _] = extra_keys
ctx = %{ctx | path: [name | ctx.path]}
Cast.error(ctx, {:unexpected_field, name})
end
end
defp check_required_fields(%{value: input_map} = ctx, schema) do
required = schema.required || []
input_keys = Map.keys(input_map)
missing_keys = required -- input_keys
if missing_keys == [] do
:ok
else
errors =
Enum.map(missing_keys, fn key ->
ctx = %{ctx | path: [key | ctx.path]}
Error.new(ctx, {:missing_field, key})
end)
{:error, ctx.errors ++ errors}
end
end
defp check_max_properties(%{schema: %{maxProperties: max_properties}} = ctx)
when is_integer(max_properties) do
count = ctx.value |> Map.keys() |> length()
if count > max_properties do
Cast.error(ctx, {:max_properties, max_properties, count})
else
:ok
end
end
defp check_max_properties(_ctx), do: :ok
defp check_min_properties(%{schema: %{minProperties: min_properties}} = ctx)
when is_integer(min_properties) do
count = ctx.value |> Map.keys() |> length()
if count < min_properties do
Cast.error(ctx, {:min_properties, min_properties, count})
else
:ok
end
end
defp check_min_properties(_ctx), do: :ok
defp cast_atom_keys(input_map, properties) do
Enum.reduce(properties, %{}, fn {key, _}, output ->
string_key = to_string(key)
case input_map do
%{^key => value} -> Map.put(output, key, value)
%{^string_key => value} -> Map.put(output, key, value)
_ -> output
end
end)
end
defp cast_properties(%{value: object, schema: schema_properties} = ctx) do
Enum.reduce(object, {:ok, %{}}, fn
{key, value}, {:ok, output} ->
cast_property(%{ctx | key: key, value: value, schema: schema_properties}, output)
_, error ->
error
end)
end
+ # Pass additional properties through when `additionalProperties` is true.
+ # Map string keys are not converted to atoms. That would require calling `String.to_atom/1`, which is not safe.
+ defp cast_additional_properties(%{schema: %{additionalProperties: ap}} = ctx, original_value)
+ when ap in [nil, true] do
+ recognized_keys = Map.keys(ctx.schema.properties || %{})
+ # Create MapSet with both atom and string versions of the property keys
+ recognized_keys = MapSet.new(recognized_keys ++ Enum.map(recognized_keys, &to_string/1))
+
+ additional_properties =
+ Enum.reduce(original_value, %{}, fn {key, value}, props ->
+ if MapSet.member?(recognized_keys, key) do
+ props
+ else
+ Map.put(props, key, value)
+ end
+ end)
+
+ updated_value = Map.merge(ctx.value, additional_properties)
+
+ %{ctx | value: updated_value}
+ end
+
+ defp cast_additional_properties(ctx, _original_value) do
+ ctx
+ end
+
defp cast_property(%{key: key, schema: schema_properties} = ctx, output) do
prop_schema = Map.get(schema_properties, key)
path = [key | ctx.path]
with {:ok, value} <- Cast.cast(%{ctx | path: path, schema: prop_schema}) do
{:ok, Map.put(output, key, value)}
end
end
defp apply_defaults(object_value, schema_properties) do
Enum.reduce(schema_properties, object_value, &apply_default/2)
end
defp apply_default({_key, %{default: nil}}, object_value), do: object_value
defp apply_default({key, %{default: default_value}}, object_value) do
if Map.has_key?(object_value, key) do
object_value
else
Map.put(object_value, key, default_value)
end
end
defp apply_default(_, object_value), do: object_value
defp to_struct(%{value: value = %_{}}), do: value
defp to_struct(%{value: value, schema: %{"x-struct": nil}}), do: value
defp to_struct(%{value: value, schema: %{"x-struct": module}}),
do: struct(module, value)
end
diff --git a/test/cast/object_test.exs b/test/cast/object_test.exs
index ab07637..ddc067a 100644
--- a/test/cast/object_test.exs
+++ b/test/cast/object_test.exs
@@ -1,205 +1,226 @@
defmodule OpenApiSpex.ObjectTest do
use ExUnit.Case
alias OpenApiSpex.{Cast, Schema}
alias OpenApiSpex.Cast.{Object, Error}
defp cast(ctx), do: Object.cast(struct(Cast, ctx))
describe "cast/3" do
test "when input is not an object" do
schema = %Schema{type: :object}
assert {:error, [error]} = cast(value: ["hello"], schema: schema)
assert %Error{} = error
assert error.reason == :invalid_type
assert error.value == ["hello"]
end
test "input map can have atom keys" do
- schema = %Schema{type: :object}
+ schema = %Schema{type: :object, properties: %{one: %Schema{type: :string}}}
assert {:ok, map} = cast(value: %{one: "one"}, schema: schema)
assert map == %{one: "one"}
end
test "converting string keys to atom keys when properties are defined" do
schema = %Schema{
type: :object,
properties: %{
one: nil
}
}
assert {:ok, map} = cast(value: %{"one" => "one"}, schema: schema)
assert map == %{one: "one"}
end
test "properties:nil, given unknown input property" do
schema = %Schema{type: :object}
assert cast(value: %{}, schema: schema) == {:ok, %{}}
assert cast(value: %{"unknown" => "hello"}, schema: schema) ==
{:ok, %{"unknown" => "hello"}}
end
test "with empty schema properties, given unknown input property" do
- schema = %Schema{type: :object, properties: %{}}
+ schema = %Schema{type: :object, properties: %{}, additionalProperties: false}
assert cast(value: %{}, schema: schema) == {:ok, %{}}
assert {:error, [error]} = cast(value: %{"unknown" => "hello"}, schema: schema)
assert %Error{} = error
assert error.reason == :unexpected_field
assert error.name == "unknown"
assert error.path == ["unknown"]
end
test "with schema properties set, given known input property" do
schema = %Schema{
type: :object,
properties: %{age: nil}
}
assert cast(value: %{}, schema: schema) == {:ok, %{}}
assert cast(value: %{"age" => "hello"}, schema: schema) == {:ok, %{age: "hello"}}
end
test "unexpected field" do
schema = %Schema{
type: :object,
- properties: %{}
+ properties: %{},
+ additionalProperties: false
}
assert {:error, [error]} = cast(value: %{foo: "foo"}, schema: schema)
assert %Error{} = error
assert error.reason == :unexpected_field
assert error.path == ["foo"]
end
test "required fields" do
schema = %Schema{
type: :object,
properties: %{age: nil, name: nil},
required: [:age, :name]
}
assert {:error, [error, error2]} = cast(value: %{}, schema: schema)
assert %Error{} = error
assert error.reason == :missing_field
assert error.name == :age
assert error.path == [:age]
assert error2.reason == :missing_field
assert error2.name == :name
assert error2.path == [:name]
end
test "fields with default values" do
schema = %Schema{
type: :object,
properties: %{name: %Schema{type: :string, default: "Rubi"}}
}
assert cast(value: %{}, schema: schema) == {:ok, %{name: "Rubi"}}
assert cast(value: %{"name" => "Jane"}, schema: schema) == {:ok, %{name: "Jane"}}
assert cast(value: %{name: "Robin"}, schema: schema) == {:ok, %{name: "Robin"}}
end
test "explicitly passing nil for fields with default values (not nullable)" do
schema = %Schema{
type: :object,
properties: %{name: %Schema{type: :string, default: "Rubi"}}
}
assert {:error, [%{reason: :null_value}]} = cast(value: %{"name" => nil}, schema: schema)
assert {:error, [%{reason: :null_value}]} = cast(value: %{name: nil}, schema: schema)
end
test "explicitly passing nil for fields with default values (nullable)" do
schema = %Schema{
type: :object,
properties: %{name: %Schema{type: :string, default: "Rubi", nullable: true}}
}
assert cast(value: %{"name" => nil}, schema: schema) == {:ok, %{name: nil}}
assert cast(value: %{name: nil}, schema: schema) == {:ok, %{name: nil}}
end
test "default values in nested schemas" do
child_schema = %Schema{
type: :object,
properties: %{name: %Schema{type: :string, default: "Rubi"}}
}
parent_schema = %Schema{
type: :object,
properties: %{child: child_schema}
}
assert cast(value: %{child: %{}}, schema: parent_schema) == {:ok, %{child: %{name: "Rubi"}}}
assert cast(value: %{child: %{"name" => "Jane"}}, schema: parent_schema) ==
{:ok, %{child: %{name: "Jane"}}}
end
test "cast property against schema" do
schema = %Schema{
type: :object,
properties: %{age: %Schema{type: :integer}}
}
assert cast(value: %{}, schema: schema) == {:ok, %{}}
assert {:error, [error]} = cast(value: %{"age" => "hello"}, schema: schema)
assert %Error{} = error
assert error.reason == :invalid_type
assert error.path == [:age]
end
+ test "allow unrecognized fields when additionalProperties is true" do
+ schema = %Schema{
+ type: :object,
+ properties: %{},
+ additionalProperties: true
+ }
+
+ assert cast(value: %{"foo" => "foo"}, schema: schema) == {:ok, %{"foo" => "foo"}}
+ end
+
+ test "allow unrecognized fields when additionalProperties is nil" do
+ schema = %Schema{
+ type: :object,
+ properties: %{},
+ additionalProperties: nil
+ }
+
+ assert cast(value: %{"foo" => "foo"}, schema: schema) == {:ok, %{"foo" => "foo"}}
+ end
+
defmodule User do
defstruct [:name]
end
test "optionally casts to struct" do
schema = %Schema{
type: :object,
"x-struct": User,
properties: %{
name: %Schema{type: :string}
}
}
assert {:ok, user} = cast(value: %{"name" => "Name"}, schema: schema)
assert user == %User{name: "Name"}
end
test "validates maxProperties" do
schema = %Schema{
type: :object,
properties: %{
one: nil,
two: nil
},
maxProperties: 1
}
assert {:error, [error]} = cast(value: %{one: "one", two: "two"}, schema: schema)
assert %Error{} = error
assert error.reason == :max_properties
assert {:ok, _} = cast(value: %{one: "one"}, schema: schema)
end
test "validates minProperties" do
schema = %Schema{
type: :object,
properties: %{
one: nil,
two: nil
},
minProperties: 1
}
assert {:error, [error]} = cast(value: %{}, schema: schema)
assert %Error{} = error
assert error.reason == :min_properties
assert {:ok, _} = cast(value: %{one: "one"}, schema: schema)
end
end
end
diff --git a/test/operation2_test.exs b/test/operation2_test.exs
index 8c52c97..17dda0a 100644
--- a/test/operation2_test.exs
+++ b/test/operation2_test.exs
@@ -1,276 +1,265 @@
defmodule OpenApiSpex.Operation2Test do
use ExUnit.Case
alias OpenApiSpex.{Operation, Operation2, Schema, Components, Reference}
alias OpenApiSpex.Cast.Error
defmodule SchemaFixtures do
@user %Schema{
type: :object,
properties: %{
user: %Schema{
type: :object,
properties: %{
email: %Schema{type: :string}
}
}
}
}
@user_list %Schema{
type: :array,
items: @user
}
@schemas %{"User" => @user, "UserList" => @user_list}
def user, do: @user
def user_list, do: @user_list
def schemas, do: @schemas
end
defmodule ParameterFixtures do
alias OpenApiSpex.{Schema, Operation}
def parameters do
%{
"member" => Operation.parameter(:member, :query, :boolean, "Membership flag")
}
end
end
defmodule OperationFixtures do
@user_index %Operation{
operationId: "UserController.index",
parameters: [
Operation.parameter(:name, :query, :string, "Filter by user name"),
Operation.parameter(:age, :query, :integer, "Filter by user age"),
%Reference{"$ref": "#/components/parameters/member"},
Operation.parameter(
:include_archived,
:query,
%Schema{type: :boolean, default: false},
"Example of a default value"
)
],
responses: %{
200 => Operation.response("User", "application/json", SchemaFixtures.user())
}
}
def user_index, do: @user_index
@create_user %Operation{
operationId: "UserController.create",
parameters: [
Operation.parameter(:name, :query, :string, "Filter by user name")
],
requestBody:
Operation.request_body("request body", "application/json", SchemaFixtures.user(),
required: true
),
responses: %{
200 => Operation.response("User list", "application/json", SchemaFixtures.user_list())
}
}
def create_user, do: @create_user
end
defmodule SpecModule do
@behaviour OpenApiSpex.OpenApi
@impl OpenApiSpex.OpenApi
def spec do
paths = %{
"/users" => %{
"post" => OperationFixtures.create_user()
}
}
%OpenApiSpex.OpenApi{
info: nil,
paths: paths,
components: %Components{
schemas: SchemaFixtures.schemas(),
parameters: ParameterFixtures.parameters()
}
}
end
end
defmodule RenderError do
def init(_) do
nil
end
def call(_conn, _errors) do
raise "should not have errors"
end
end
describe "cast/4" do
test "cast request body" do
conn = create_conn(%{"user" => %{"email" => "foo@bar.com"}})
assert {:ok, conn} =
Operation2.cast(
OperationFixtures.create_user(),
conn,
"application/json",
SpecModule.spec().components
)
assert %Plug.Conn{} = conn
end
test "cast request body - invalid data type" do
conn = create_conn(%{"user" => %{"email" => 123}})
assert {:error, errors} =
Operation2.cast(
OperationFixtures.create_user(),
conn,
"application/json",
SpecModule.spec().components
)
assert [error] = errors
assert %Error{} = error
assert error.reason == :invalid_type
end
test "casts valid query params and respects defaults" do
valid_query_params = %{"name" => "Rubi", "age" => "31", "member" => "true"}
assert {:ok, conn} = do_index_cast(valid_query_params)
assert conn.params == %{age: 31, member: true, name: "Rubi", include_archived: false}
end
test "casts valid query params and overrides defaults" do
valid_query_params = %{
"name" => "Rubi",
"age" => "31",
"member" => "true",
"include_archived" => "true"
}
assert {:ok, conn} = do_index_cast(valid_query_params)
assert conn.params == %{age: 31, member: true, name: "Rubi", include_archived: true}
end
- test "validate undefined query param name" do
- query_params = %{"unknown" => "asdf"}
-
- assert {:error, [error]} = do_index_cast(query_params)
-
- assert %Error{} = error
- assert error.reason == :unexpected_field
- assert error.name == "unknown"
- assert error.path == ["unknown"]
- end
-
test "validate invalid data type for query param" do
query_params = %{"age" => "asdf"}
assert {:error, [error]} = do_index_cast(query_params)
assert %Error{} = error
assert error.reason == :invalid_type
assert error.type == :integer
assert error.value == "asdf"
end
test "validate missing required query param" do
parameter =
Operation.parameter(:name, :query, :string, "Filter by user name", required: true)
operation = %{OperationFixtures.user_index() | parameters: [parameter]}
assert {:error, [error]} = do_index_cast(%{}, operation: operation)
assert %Error{} = error
assert error.reason == :missing_field
assert error.name == :name
end
test "validate missing content-type header for required requestBody" do
conn = :post |> Plug.Test.conn("/api/users/") |> Plug.Conn.fetch_query_params()
operation = OperationFixtures.create_user()
assert {:error, [%Error{reason: :missing_header, name: "content-type"}]} =
Operation2.cast(
operation,
conn,
nil,
SpecModule.spec().components
)
end
test "validate invalid content-type header for required requestBody" do
conn =
create_conn(%{})
|> Plug.Conn.put_req_header("content-type", "text/html")
operation = OperationFixtures.create_user()
assert {:error, [%Error{reason: :invalid_header, name: "content-type"}]} =
Operation2.cast(
operation,
conn,
"text/html",
SpecModule.spec().components
)
end
test "validate invalid value for integer range" do
parameter =
Operation.parameter(
:age,
:query,
%Schema{type: :integer, minimum: 1, maximum: 99},
"Filter by user age",
required: true
)
operation = %{OperationFixtures.user_index() | parameters: [parameter]}
assert {:error, [error]} = do_index_cast(%{"age" => 100}, operation: operation)
assert %Error{} = error
assert error.reason == :maximum
assert {:error, [error]} = do_index_cast(%{"age" => 0}, operation: operation)
assert %Error{} = error
assert error.reason == :minimum
end
defp do_index_cast(query_params, opts \\ []) do
conn =
:get
|> Plug.Test.conn("/api/users?" <> URI.encode_query(query_params))
|> Plug.Conn.put_req_header("content-type", "application/json")
|> Plug.Conn.fetch_query_params()
|> build_params()
operation = opts[:operation] || OperationFixtures.user_index()
Operation2.cast(
operation,
conn,
"application/json",
SpecModule.spec().components
)
end
defp create_conn(body_params) do
:post
|> Plug.Test.conn("/api/users")
|> Plug.Conn.put_req_header("content-type", "application/json")
|> Plug.Conn.fetch_query_params()
|> Map.put(:body_params, body_params)
|> build_params()
end
defp build_params(conn) do
params =
conn.path_params
|> Map.merge(conn.query_params)
|> Map.merge(conn.body_params)
%{conn | params: params}
end
end
end
diff --git a/test/plug/cast_and_validate/custom_error_user_controller_test.exs b/test/plug/cast_and_validate/custom_error_user_controller_test.exs
index d4ca93e..b07546b 100644
--- a/test/plug/cast_and_validate/custom_error_user_controller_test.exs
+++ b/test/plug/cast_and_validate/custom_error_user_controller_test.exs
@@ -1,37 +1,37 @@
defmodule OpenApiSpex.Plug.CastAndValidate.CustomErrorUserControllerTest do
use ExUnit.Case, async: true
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
@tag :capture_log
test "Invalid value" 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
@tag :capture_log
test "Invalid Param" do
conn =
:get
- |> Plug.Test.conn("/api/custom_error_users?validParam=123&inValidParam=123&inValid2=hi")
+ |> Plug.Test.conn("/api/custom_error_users?validParam=123")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 400
- assert conn.resp_body == "Unexpected field: inValid2"
+ assert conn.resp_body == "Invalid boolean. Got: string"
end
end
end
diff --git a/test/plug/cast_and_validate_test.exs b/test/plug/cast_and_validate_test.exs
index c2c5467..abb611c 100644
--- a/test/plug/cast_and_validate_test.exs
+++ b/test/plug/cast_and_validate_test.exs
@@ -1,159 +1,159 @@
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&inValidParam=123&inValid2=hi")
+ |> 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" => "Unexpected field: inValid2",
- "source" => %{"pointer" => "/inValid2"},
+ "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&inValidParam=123&inValid2=hi")
+ |> Plug.Test.conn("/api/custom_error_users?validParam=123")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 400
- assert conn.resp_body == "Unexpected field: inValid2"
+ 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
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Wed, Nov 27, 3:40 PM (1 d, 19 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40681
Default Alt Text
(33 KB)
Attached To
Mode
R22 open_api_spex
Attached
Detach File
Event Timeline
Log In to Comment