Packages
open_api_spex
3.22.0
3.22.3
3.22.2
3.22.1
3.22.0
3.21.5
3.21.4
3.21.3
3.21.2
3.21.1
3.21.0
3.20.1
3.20.0
3.19.1
3.19.0
3.18.3
3.18.2
3.18.1
3.18.0
3.17.3
3.17.2
3.17.1
3.17.0
3.16.4
3.16.3
3.16.2
3.16.1
3.16.0
3.15.0
3.14.0
3.13.0
3.12.0
3.11.0
3.10.0
3.9.0
3.8.0
3.7.0
3.6.0
3.5.2
3.5.1
3.5.0
3.4.0
3.3.0
3.2.1
3.2.0
3.1.0
3.0.0
2.3.1
2.3.0
2.2.0
2.1.1
2.1.0
2.0.0
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.1
1.0.0
Leverage Open Api Specification 3 (swagger) to document, test, validate and explore your Plug and Phoenix APIs.
Current section
Files
Jump to
Current section
Files
lib/open_api_spex/cast/object.ex
defmodule OpenApiSpex.Cast.Object do
@moduledoc false
alias OpenApiSpex.Cast
alias OpenApiSpex.Cast.Utils
alias OpenApiSpex.Reference
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, additionalProperties: nil}}) do
{:ok, value}
end
def cast(ctx) do
ctx = handle_struct_value(ctx)
with :ok <- check_unrecognized_properties(ctx),
ctx = resolve_schema_properties_references(ctx),
ctx = cast_atom_keys(ctx),
{:ok, ctx} <- cast_additional_properties(ctx),
:ok <- Utils.check_required_fields(ctx),
:ok <- check_max_properties(ctx),
:ok <- check_min_properties(ctx),
{:ok, ctx} <- cast_properties(ctx) do
ctx =
ctx
|> apply_defaults()
|> to_struct()
{:ok, ctx}
end
end
defp resolve_schema_properties_references(%{schema: schema, schemas: schemas} = ctx) do
schema_properties = schema.properties || %{}
resolved_schema_properties =
Enum.reduce(schema_properties, schema_properties, fn property, properties ->
resolve_property_if_reference(property, properties, schemas)
end)
%{ctx | schema: %{schema | properties: resolved_schema_properties}}
end
defp resolve_property_if_reference({key, %Reference{} = reference}, properties, schemas) do
Map.put(properties, key, Reference.resolve_schema(reference, schemas))
end
defp resolve_property_if_reference(_not_a_reference, properties, _schemas), do: properties
# When additionalProperties is not false, extra properties are allowed in input
defp check_unrecognized_properties(%{schema: %{additionalProperties: ap}}) when ap != false do
:ok
end
defp check_unrecognized_properties(%{value: value, schema: schema} = ctx) do
schema_properties = schema.properties || %{}
input_keys = value |> Map.keys() |> Enum.map(&to_string/1)
schema_keys = schema_properties |> 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_max_properties(%{schema: %{maxProperties: max_properties}} = ctx)
when is_integer(max_properties) do
count = map_size(ctx.value)
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 = map_size(ctx.value)
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(%{value: input_map, schema: %{properties: properties}} = ctx) do
value =
Enum.reduce(properties, input_map, fn {key, _}, output ->
string_key = to_string(key)
if Map.has_key?(output, string_key) do
{value, output} = Map.pop!(output, string_key)
Map.put(output, key, value)
else
output
end
end)
%{ctx | value: value}
end
defp cast_properties(%{value: object, schema: %{properties: schema_properties}} = ctx) do
object
|> Enum.reduce({%{}, []}, fn {key, value}, {output, object_errors} ->
case cast_property(%{ctx | key: key, value: value, schema: schema_properties}, output) do
{:ok, output} ->
{output, object_errors}
{:error, errors} ->
{output, object_errors ++ errors}
end
end)
|> case do
{value, []} ->
{:ok, %{ctx | value: value}}
{_, errors} ->
{:error, errors}
end
end
defp cast_additional_properties(%{value: value, schema: %{additionalProperties: ap}} = ctx) do
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)
_, error ->
error
end)
end
defp get_additional_properties(value, ctx) do
recognized_keys =
(ctx.schema.properties || %{})
|> Map.keys()
|> Enum.flat_map(&[&1, to_string(&1)])
|> MapSet.new()
for {key, _value} = prop <- 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_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(%{opts: opts, value: value, schema: %{properties: properties}} = ctx) do
value =
if Keyword.get(opts, :apply_defaults, true) do
Enum.reduce(properties, value, &apply_default/2)
else
value
end
%{ctx | value: value}
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": module}}) when not is_nil(module),
do: struct(module, value)
defp to_struct(%{value: value}), do: value
defp handle_struct_value(%{value: %_{} = value, schema: %{"x-struct": module}} = ctx)
when not is_nil(module) do
%{ctx | value: Map.from_struct(value)}
end
defp handle_struct_value(%{value: %struct{} = value, schema: schema} = ctx) do
%{ctx | value: Map.from_struct(value), schema: %{schema | "x-struct": struct}}
end
defp handle_struct_value(ctx), do: ctx
end