Page MenuHomePhorge

No OneTemporary

Size
15 KB
Referenced Files
None
Subscribers
None
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

Mime Type
text/x-diff
Expires
Tue, Nov 26, 5:44 AM (1 d, 11 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40183
Default Alt Text
(15 KB)

Event Timeline