Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix oneOf/allOf/anyOf and schema module in Discriminator mapping #455

Merged
merged 34 commits into from
Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
024e7d2
fix: cast discriminator when value has atom keys
igormq Aug 16, 2021
bfba2b9
chore: addressing pr comments
Aug 23, 2021
53490dd
fix: add new logic for one of and any of cast
igormq Nov 30, 2021
876bf78
chore: fix format
igormq Nov 30, 2021
5c33b97
chore: fix format
igormq Nov 30, 2021
44de9df
fix: all of with required
igormq Dec 15, 2021
1d83763
fix: small fix on the allOf function
robpmarques Dec 16, 2021
44867b7
fix: fixing required fields outside all_of, one_of and any_of
robpmarques Dec 16, 2021
f4ea70b
chore: adding put_properties to inherit properties on all of, any of …
robpmarques Dec 16, 2021
13a97b2
fix: adding put_properties
robpmarques Dec 16, 2021
b87cf1c
fix: removing put_properties
robpmarques Dec 19, 2021
53f56ea
chore: make a better approach to test for errors in required fields
igormq Dec 23, 2021
36467a5
chore: one of should return errors
igormq Jan 24, 2022
f853eb7
chore: expose all errors
igormq Jan 25, 2022
58388d0
fix: anyOf and allOf casting with additionalProperties
albertored Mar 30, 2022
20bdf66
fix format
albertored Mar 30, 2022
10d5d29
fix: correctly handle write/readOnly on checking required properties
albertored Mar 30, 2022
4d33362
fix: it should be possible to cast a null value when nullabe true in …
albertored Mar 30, 2022
85cfd24
fix: no special treatment for allOf of arrays
albertored Mar 30, 2022
cc1a6c3
fix: nil value when schema has xxxOf
albertored Mar 30, 2022
3a10680
Merge branch 'master' into fix-xxxOf
lucacorti May 23, 2022
ad7547b
Don't prepend discriminator propertyName to path
lucacorti Jun 6, 2022
ed48cc9
Fix tests
lucacorti Jun 6, 2022
5cf10bd
Merge branch 'master' into fix-xxxOf
lucacorti Jul 5, 2022
071d7c5
Fix merge
lucacorti Jul 7, 2022
9599e57
Format and fix tests
lucacorti Jul 7, 2022
55e56f2
Merge branch 'master' into fix-xxxOf
lucacorti Jul 21, 2022
489e93a
Merge branch 'master' into fix-xxxOf
lucacorti Jul 28, 2022
895e2f5
Merge branch 'open-api-spex:master' into fix-xxxOf
lucacorti Jul 31, 2022
75db236
Merge branch 'open-api-spex:master' into fix-xxxOf
lucacorti Aug 3, 2022
7eb02e4
Merge branch 'open-api-spex:master' into fix-xxxOf
lucacorti Sep 26, 2022
33ec112
Address review remarks
lucacorti Sep 26, 2022
d2402d3
Remove casting from atom-keyed maps
mbuhot Oct 19, 2022
cd94ab5
chore: formatting
mbuhot Oct 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions lib/open_api_spex/cast.ex
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,17 @@ defmodule OpenApiSpex.Cast do
{:ok, nil}
end

# nullable: false
def cast(%__MODULE__{value: nil} = ctx) do
error(ctx, {:null_value})
# nullable not present in root schema or equal to false
# dispatch to xxxOf modules if corresponding key is present
# or return error
def cast(%__MODULE__{value: nil, schema: schema} = ctx) do
cond do
match?(%{nullable: false}, schema) -> error(ctx, {:null_value})
match?(%{oneOf: list} when is_list(list), schema) -> OneOf.cast(ctx)
match?(%{anyOf: list} when is_list(list), schema) -> AnyOf.cast(ctx)
match?(%{allOf: list} when is_list(list), schema) -> AllOf.cast(ctx)
true -> error(ctx, {:null_value})
end
zorbash marked this conversation as resolved.
Show resolved Hide resolved
end

# Enum
Expand Down
85 changes: 23 additions & 62 deletions lib/open_api_spex/cast/all_of.ex
Original file line number Diff line number Diff line change
@@ -1,49 +1,18 @@
defmodule OpenApiSpex.Cast.AllOf do
@moduledoc false
alias OpenApiSpex.Cast
alias OpenApiSpex.Cast.Utils
alias OpenApiSpex.Schema

def cast(ctx), do: cast_all_of(ctx, nil)

defp cast_all_of(%{schema: %{allOf: [%Schema{type: :array} = schema | remaining]}} = ctx, acc)
when is_list(acc) or acc == nil do
# Since we parse a multi-type array, acc has to be a list or nil
acc = acc || []

case Cast.cast(%{ctx | schema: schema}) do
{:ok, value} when is_list(value) ->
# Since the cast for the list didn't result in a cast error,
# we do not proceed the values through the remaining schemas
{:ok, Enum.concat(acc, value)}

{:error, errors} ->
with {:ok, cleaned_ctx} <- reject_error_values(ctx, errors),
{:ok, cleaned_values} <- Cast.cast(cleaned_ctx) do
new_ctx = put_in(ctx.schema.allOf, remaining)
new_ctx = update_in(new_ctx.value, fn values -> values -- cleaned_ctx.value end)
cast_all_of(new_ctx, Enum.concat(acc, cleaned_values))
else
_ -> Cast.error(ctx, {:all_of, to_string(schema.title || schema.type)})
end
end
end

defp cast_all_of(%{schema: %{allOf: [%Schema{} = schema | remaining]}} = ctx, acc) do
relaxed_schema = %{schema | additionalProperties: true, "x-struct": nil}
relaxed_schema = %{schema | "x-struct": nil}
new_ctx = put_in(ctx.schema.allOf, remaining)

case Cast.cast(%{ctx | schema: relaxed_schema}) do
case Cast.cast(%{ctx | errors: [], schema: relaxed_schema}) do
{:ok, value} when is_map(value) ->
# Complex allOf Schema

# reject all "additionalProperties"
acc =
value
|> Enum.reject(fn {k, _} -> is_binary(k) end)
|> Enum.concat(acc || %{})
|> Map.new()

cast_all_of(new_ctx, acc)
cast_all_of(new_ctx, Utils.merge_maps(acc || %{}, value))

{:ok, value} ->
# allOf definitions with primitives are a little bit strange.
Expand All @@ -52,41 +21,33 @@ defmodule OpenApiSpex.Cast.AllOf do
# must be compatible with all Schemas
cast_all_of(new_ctx, acc || value)

{:error, _} ->
# Since in a allOf Schema, every
Cast.error(ctx, {:all_of, to_string(relaxed_schema.title || relaxed_schema.type)})
{:error, errors} ->
Cast.error(
%Cast{ctx | errors: ctx.errors ++ errors},
{:all_of, to_string(relaxed_schema.title || relaxed_schema.type)}
)
end
end

defp cast_all_of(%{schema: %{allOf: [schema | remaining]}} = ctx, result) do
schema = OpenApiSpex.resolve_schema(schema, ctx.schemas)
cast_all_of(%{ctx | schema: %{allOf: [schema | remaining]}}, result)
defp cast_all_of(%{schema: %{allOf: [nested_schema | remaining]} = schema} = ctx, result) do
nested_schema = OpenApiSpex.resolve_schema(nested_schema, ctx.schemas)
cast_all_of(%{ctx | schema: %{schema | allOf: [nested_schema | remaining]}}, result)
end

defp cast_all_of(%{schema: schema} = ctx, nil) do
Cast.error(ctx, {:all_of, to_string(schema.title || schema.type)})
end

defp cast_all_of(%{schema: %{allOf: [], "x-struct": module}}, acc) when not is_nil(module),
do: {:ok, struct(module, acc)}

defp cast_all_of(%{schema: %{allOf: []}}, acc) do
# All values have been casted against the allOf schemas - return accumulator
{:ok, acc}
end

defp reject_error_values(%{value: values} = ctx, [%{reason: :invalid_type} = error | tail]) do
new_values = List.delete(values, error.value)
reject_error_values(%{ctx | value: new_values}, tail)
defp cast_all_of(%{schema: %{allOf: []}, errors: []} = ctx, acc) do
with :ok <- Utils.check_required_fields(ctx, acc) do
{:ok, acc}
end
end

defp reject_error_values(ctx, []) do
# All errors should now be resolved for the current schema
{:ok, ctx}
defp cast_all_of(%{schema: %{allOf: [], errors: [], "x-struct": module}} = ctx, acc)
when not is_nil(module) do
with :ok <- Utils.check_required_fields(ctx, acc) do
{:ok, acc}
end
end

defp reject_error_values(_ctx, errors) do
# Some errors couldn't be resolved, we break and return the remaining errors
errors
defp cast_all_of(%{schema: schema} = ctx, _acc) do
Cast.error(ctx, {:all_of, to_string(schema.title || schema.type)})
end
end
62 changes: 41 additions & 21 deletions lib/open_api_spex/cast/any_of.ex
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
defmodule OpenApiSpex.Cast.AnyOf do
@moduledoc false
alias OpenApiSpex.Cast
alias OpenApiSpex.Cast.Utils
alias OpenApiSpex.Schema

def cast(ctx, failed_schemas \\ [], acc \\ nil), do: cast_any_of(ctx, failed_schemas, acc)
def cast(ctx, failed_schemas \\ [], acc \\ :__not_casted),
do: cast_any_of(ctx, failed_schemas, acc)

defp cast_any_of(%_{schema: %{anyOf: []}} = ctx, failed_schemas, nil) do
defp cast_any_of(%_{schema: %{anyOf: []}} = ctx, failed_schemas, :__not_casted) do
Cast.error(ctx, {:any_of, error_message(failed_schemas, ctx.schemas)})
end

Expand All @@ -14,40 +16,55 @@ defmodule OpenApiSpex.Cast.AnyOf do
failed_schemas,
acc
) do
relaxed_schema = %{schema | additionalProperties: true, "x-struct": nil}
relaxed_schema = %{schema | "x-struct": nil}
new_ctx = put_in(ctx.schema.anyOf, remaining)

case Cast.cast(%{ctx | schema: relaxed_schema}) do
case Cast.cast(%{ctx | errors: [], schema: relaxed_schema}) do
{:ok, value} when is_struct(value) ->
{:ok, value}

{:ok, value} when is_map(value) ->
acc =
value
|> Enum.reject(fn {k, _} -> is_binary(k) end)
|> Enum.concat(acc || %{})
|> Map.new()

acc = acc |> value_if_not_casted(%{}) |> Utils.merge_maps(value)
cast_any_of(new_ctx, failed_schemas, acc)

{:ok, value} ->
cast_any_of(new_ctx, failed_schemas, acc || value)

{:error, _} ->
cast_any_of(new_ctx, [schema | failed_schemas], acc)
cast_any_of(new_ctx, failed_schemas, value_if_not_casted(acc, value))

{:error, errors} ->
cast_any_of(
%Cast{new_ctx | errors: new_ctx.errors ++ errors},
[schema | failed_schemas],
acc
)
end
end

defp cast_any_of(%{schema: %{anyOf: [schema | remaining]}} = ctx, failed_schemas, acc) do
schema = OpenApiSpex.resolve_schema(schema, ctx.schemas)
cast_any_of(%{ctx | schema: %{anyOf: [schema | remaining]}}, failed_schemas, acc)
defp cast_any_of(
%{schema: %{anyOf: [nested_schema | remaining]} = schema} = ctx,
failed_schemas,
acc
) do
nested_schema = OpenApiSpex.resolve_schema(nested_schema, ctx.schemas)

cast_any_of(
%{ctx | schema: %{schema | anyOf: [nested_schema | remaining]}},
failed_schemas,
acc
)
end

defp cast_any_of(%_{schema: %{anyOf: [], "x-struct": module}}, _failed_schemas, acc)
when not is_nil(module),
do: {:ok, struct(module, acc)}
defp cast_any_of(%_{schema: %{anyOf: [], "x-struct": module}} = ctx, _failed_schemas, acc)
when not is_nil(module) do
with :ok <- Utils.check_required_fields(ctx, acc) do
{:ok, struct(module, acc)}
end
end

defp cast_any_of(%_{schema: %{anyOf: []}}, _failed_schemas, acc), do: {:ok, acc}
defp cast_any_of(%_{schema: %{anyOf: []}} = ctx, _failed_schemas, acc) do
with :ok <- Utils.check_required_fields(ctx, acc) do
{:ok, acc}
end
end

## Private functions

Expand All @@ -69,4 +86,7 @@ defmodule OpenApiSpex.Cast.AnyOf do
end
|> Enum.join(", ")
end

defp value_if_not_casted(:__not_casted, value), do: value
defp value_if_not_casted(value, _), do: value
end
25 changes: 15 additions & 10 deletions lib/open_api_spex/cast/discriminator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,14 @@ defmodule OpenApiSpex.Cast.Discriminator do
defp cast_discriminator(%_{value: value, schema: schema} = ctx) do
{discriminator_property, mappings} = discriminator_details(schema)

case Map.pop(value, "#{discriminator_property}") do
{"", _} ->
case value["#{discriminator_property}"] || value[discriminator_property] do
v when v in ["", nil] ->
error(:no_value_for_discriminator, ctx)

{discriminator_value, _castable_value} ->
discriminator_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]
}
composite_ctx = %{ctx | schema: %{schema | discriminator: nil}}

cast_composition(composite_ctx, ctx, discriminator_value, mappings)
end
Expand All @@ -60,7 +56,12 @@ defmodule OpenApiSpex.Cast.Discriminator do
defp cast_composition(composite_ctx, ctx, discriminator_value, mappings) do
with {composite_schemas, cast_composition_result} <- cast_composition(composite_ctx),
{:ok, _} <- cast_composition_result,
%{} = schema <- find_discriminator_schema(discriminator_value, mappings, composite_schemas) do
%{} = schema <-
find_discriminator_schema(
discriminator_value,
mappings,
composite_schemas
) do
Cast.cast(%{composite_ctx | schema: schema})
else
nil -> error(:invalid_discriminator_value, ctx)
Expand Down Expand Up @@ -93,7 +94,11 @@ defmodule OpenApiSpex.Cast.Discriminator do
end
end

defp find_discriminator_schema(discriminator, _, schemas) do
defp find_discriminator_schema(discriminator, _, schemas) when is_atom(discriminator) do
Enum.find(schemas, &Kernel.==(&1."x-struct", discriminator))
lucacorti marked this conversation as resolved.
Show resolved Hide resolved
end

defp find_discriminator_schema(discriminator, _, schemas) when is_binary(discriminator) do
Enum.find(schemas, &Kernel.==(&1.title, discriminator))
end

Expand Down
33 changes: 2 additions & 31 deletions lib/open_api_spex/cast/object.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule OpenApiSpex.Cast.Object do
@moduledoc false
alias OpenApiSpex.Cast
alias OpenApiSpex.Cast.Error
alias OpenApiSpex.Cast.Utils
alias OpenApiSpex.Reference

def cast(%{value: value} = ctx) when not is_map(value) do
Expand All @@ -22,7 +22,7 @@ defmodule OpenApiSpex.Cast.Object do
value = cast_atom_keys(value, resolved_schema_properties),
ctx = %{ctx | value: value},
{:ok, ctx} <- cast_additional_properties(ctx, original_value),
:ok <- check_required_fields(ctx, schema),
:ok <- Utils.check_required_fields(ctx),
:ok <- check_max_properties(ctx),
:ok <- check_min_properties(ctx),
{:ok, value} <- cast_properties(%{ctx | schema: resolved_schema_properties}) do
Expand Down Expand Up @@ -70,35 +70,6 @@ defmodule OpenApiSpex.Cast.Object do
end
end

defp check_required_fields(%{value: input_map} = ctx, schema) do
required = schema.required || []

# Adjust required fields list, based on read_write_scope
required =
Enum.filter(required, fn key ->
case {ctx.read_write_scope, schema.properties[key]} do
{:read, %{writeOnly: true}} -> false
{:write, %{readOnly: true}} -> false
_ -> true
end
end)

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()
Expand Down
21 changes: 13 additions & 8 deletions lib/open_api_spex/cast/one_of.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule OpenApiSpex.Cast.OneOf do
@moduledoc false
alias OpenApiSpex.Cast
alias OpenApiSpex.Cast.Utils
alias OpenApiSpex.Schema

def cast(%_{schema: %{type: _, oneOf: []}} = ctx) do
Expand All @@ -9,20 +10,24 @@ defmodule OpenApiSpex.Cast.OneOf do

def cast(%{schema: %{type: _, oneOf: schemas}} = ctx) do
castable_schemas =
Enum.reduce(schemas, {[], []}, fn schema, {results, error_schemas} ->
Enum.reduce(schemas, {ctx, [], []}, fn schema, {ctx, results, error_schemas} ->
schema = OpenApiSpex.resolve_schema(schema, ctx.schemas)
relaxed_schema = %{schema | "x-struct": nil}

case Cast.cast(%{ctx | schema: schema}) do
{:ok, value} -> {[{:ok, value, schema} | results], error_schemas}
_error -> {results, [schema | error_schemas]}
with {:ok, value} <- Cast.cast(%{ctx | errors: [], schema: relaxed_schema}),
:ok <- Utils.check_required_fields(ctx, value) do
{ctx, [{:ok, value, schema} | results], error_schemas}
else
{:error, errors} ->
{%Cast{ctx | errors: ctx.errors ++ errors}, results, [schema | error_schemas]}
end
end)

case castable_schemas do
{[{:ok, %_{} = value, _}], _} -> {:ok, value}
{[{:ok, value, %Schema{"x-struct": nil}}], _} -> {:ok, value}
{[{:ok, value, %Schema{"x-struct": module}}], _} -> {:ok, struct(module, value)}
{success_results, failed_schemas} -> error(ctx, success_results, failed_schemas)
{_, [{:ok, %_{} = value, _}], _} -> {:ok, value}
{_, [{:ok, value, %Schema{"x-struct": nil}}], _} -> {:ok, value}
{_, [{:ok, value, %Schema{"x-struct": module}}], _} -> {:ok, struct(module, value)}
{ctx, success_results, failed_schemas} -> error(ctx, success_results, failed_schemas)
end
end

Expand Down
Loading