Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F114378
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
15 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/open_api_spex/cast/object.ex b/lib/open_api_spex/cast/object.ex
index e2f1aac..7cad1d2 100644
--- a/lib/open_api_spex/cast/object.ex
+++ b/lib/open_api_spex/cast/object.ex
@@ -1,174 +1,184 @@
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, 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
+ # When additionalProperties is not false, extra properties are allowed in input
defp check_unrecognized_properties(%{schema: %{additionalProperties: ap}}, _expected_keys)
- when ap in [nil, true] do
+ when ap != false 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))
+ defp cast_additional_properties(%{schema: %{additionalProperties: ap}} = ctx, original_value) do
+ original_value
+ |> get_additional_properties(ctx)
+ |> Enum.reduce({:ok, ctx}, fn
+ {key, value}, {:ok, ctx} ->
+ ap_cast_context = %{ctx | key: key, value: value, path: [key | ctx.path], schema: ap}
+ cast_additional_property(ap_cast_context, ctx)
- 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)
+ _, error ->
+ error
+ end)
+ end
- updated_value = Map.merge(ctx.value, additional_properties)
+ defp get_additional_properties(original_value, ctx) do
+ recognized_keys =
+ (ctx.schema.properties || %{})
+ |> Map.keys()
+ |> Enum.flat_map(&[&1, to_string(&1)])
+ |> MapSet.new()
- %{ctx | value: updated_value}
+ for {key, _value} = prop <- original_value,
+ not MapSet.member?(recognized_keys, key) do
+ prop
+ end
+ end
+
+ defp cast_additional_property(%{schema: ap} = ctx, output_ctx) when is_map(ap) do
+ with {:ok, value} <- Cast.cast(ctx) do
+ {:ok, %{output_ctx | value: Map.put(output_ctx.value, ctx.key, value)}}
+ end
end
- defp cast_additional_properties(ctx, _original_value) do
- ctx
+ defp cast_additional_property(%{schema: ap} = ctx, output_ctx) when ap in [nil, true] do
+ {:ok, %{output_ctx | value: Map.put(output_ctx.value, ctx.key, ctx.value)}}
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 ddc067a..ff13009 100644
--- a/test/cast/object_test.exs
+++ b/test/cast/object_test.exs
@@ -1,226 +1,261 @@
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, 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: %{}, 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: %{},
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
+ test "casts additional properties according to the additionalProperty schema" do
+ schema = %Schema{
+ type: :object,
+ properties: %{},
+ additionalProperties: %Schema{
+ type: :object,
+ properties: %{
+ age: %Schema{type: :integer}
+ }
+ }
+ }
+
+ input = %{"alex" => %{"age" => 42}, "brian" => %{"age" => 43}}
+
+ assert cast(value: input, schema: schema) ==
+ {:ok, %{"alex" => %{age: 42}, "brian" => %{age: 43}}}
+ end
+
+ test "when additionalProperty schema does not work out" do
+ schema = %Schema{
+ type: :object,
+ properties: %{},
+ additionalProperties: %Schema{
+ type: :object,
+ properties: %{},
+ additionalProperties: %Schema{type: :integer}
+ }
+ }
+
+ input = %{"alex" => %{"age" => 5, "size" => 5}, "brian" => %{"age" => 6, "size" => "tall"}}
+
+ assert {:error, [%{path: ["brian", "size"], reason: :invalid_type}]} =
+ cast(value: input, schema: schema)
+ 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
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Tue, Nov 26, 5:44 AM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40183
Default Alt Text
(15 KB)
Attached To
Mode
R22 open_api_spex
Attached
Detach File
Event Timeline
Log In to Comment