Skip to content

Commit e8a3538

Browse files
authored
sets read_write_scope from opts, this will permit to comply to readOnly (#572)
1 parent 10042c2 commit e8a3538

File tree

5 files changed

+108
-59
lines changed

5 files changed

+108
-59
lines changed

lib/open_api_spex.ex

+8-3
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,17 @@ defmodule OpenApiSpex do
7272
@doc """
7373
Cast and validate a value against a given Schema belonging to a given OpenApi spec.
7474
"""
75-
def cast_value(value, schema = %schema_mod{}, spec = %OpenApi{})
75+
def cast_value(value, schema = %schema_mod{}, spec = %OpenApi{}, opts \\ [])
7676
when schema_mod in [Schema, Reference] do
77-
OpenApiSpex.Cast.cast(schema, value, spec.components.schemas)
77+
OpenApiSpex.Cast.cast(schema, value, spec.components.schemas, opts)
7878
end
7979

80-
@type cast_opt :: {:replace_params, boolean()} | {:apply_defaults, boolean()}
80+
@type read_write_scope :: nil | :read | :write
81+
82+
@type cast_opt ::
83+
{:replace_params, boolean()}
84+
| {:apply_defaults, boolean()}
85+
| {:read_write_scope, read_write_scope()}
8186

8287
@spec cast_and_validate(
8388
OpenApi.t(),

lib/open_api_spex/cast.ex

+11-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ defmodule OpenApiSpex.Cast do
2222

2323
@type schema_or_reference :: Schema.t() | Reference.t()
2424

25-
@type cast_opt :: {:apply_defaults, boolean()}
25+
@type cast_opt :: {:apply_defaults, boolean()} | {:read_write_scope, read_write_scope()}
2626

2727
@type t :: %__MODULE__{
2828
value: term(),
@@ -103,7 +103,16 @@ defmodule OpenApiSpex.Cast do
103103
@spec cast(schema_or_reference | nil, term(), map(), [cast_opt()]) ::
104104
{:ok, term()} | {:error, [Error.t()]}
105105
def cast(schema, value, schemas \\ %{}, opts \\ []) do
106-
ctx = %__MODULE__{schema: schema, value: value, schemas: schemas, opts: opts}
106+
read_write_scope = Keyword.get(opts, :read_write_scope)
107+
108+
ctx = %__MODULE__{
109+
schema: schema,
110+
value: value,
111+
schemas: schemas,
112+
read_write_scope: read_write_scope,
113+
opts: opts
114+
}
115+
107116
cast(ctx)
108117
end
109118

lib/open_api_spex/cast/object.ex

+68-53
Original file line numberDiff line numberDiff line change
@@ -12,36 +12,35 @@ defmodule OpenApiSpex.Cast.Object do
1212
{:ok, value}
1313
end
1414

15-
def cast(%{value: value, schema: schema, schemas: schemas} = ctx) do
16-
original_value = value
17-
schema_properties = schema.properties || %{}
15+
def cast(ctx) do
16+
ctx = handle_struct_value(ctx)
1817

19-
with :ok <- check_unrecognized_properties(ctx, schema_properties),
20-
resolved_schema_properties <-
21-
resolve_schema_properties_references(schema_properties, schemas),
22-
value = cast_atom_keys(value, resolved_schema_properties),
23-
ctx = %{ctx | value: value},
24-
{:ok, ctx} <- cast_additional_properties(ctx, original_value),
18+
with :ok <- check_unrecognized_properties(ctx),
19+
ctx = resolve_schema_properties_references(ctx),
20+
ctx = cast_atom_keys(ctx),
21+
{:ok, ctx} <- cast_additional_properties(ctx),
2522
:ok <- Utils.check_required_fields(ctx),
2623
:ok <- check_max_properties(ctx),
2724
:ok <- check_min_properties(ctx),
28-
{:ok, value} <- cast_properties(%{ctx | schema: resolved_schema_properties}) do
29-
value_with_defaults =
30-
if Keyword.get(ctx.opts, :apply_defaults, true) do
31-
apply_defaults(value, resolved_schema_properties)
32-
else
33-
value
34-
end
25+
{:ok, ctx} <- cast_properties(ctx) do
26+
ctx =
27+
ctx
28+
|> apply_defaults()
29+
|> to_struct()
3530

36-
ctx = to_struct(%{ctx | value: value_with_defaults}, original_value)
3731
{:ok, ctx}
3832
end
3933
end
4034

41-
defp resolve_schema_properties_references(schema_properties, schemas) do
42-
Enum.reduce(schema_properties, schema_properties, fn property, properties ->
43-
resolve_property_if_reference(property, properties, schemas)
44-
end)
35+
defp resolve_schema_properties_references(%{schema: schema, schemas: schemas} = ctx) do
36+
schema_properties = schema.properties || %{}
37+
38+
resolved_schema_properties =
39+
Enum.reduce(schema_properties, schema_properties, fn property, properties ->
40+
resolve_property_if_reference(property, properties, schemas)
41+
end)
42+
43+
%{ctx | schema: %{schema | properties: resolved_schema_properties}}
4544
end
4645

4746
defp resolve_property_if_reference({key, %Reference{} = reference}, properties, schemas) do
@@ -51,14 +50,14 @@ defmodule OpenApiSpex.Cast.Object do
5150
defp resolve_property_if_reference(_not_a_reference, properties, _schemas), do: properties
5251

5352
# When additionalProperties is not false, extra properties are allowed in input
54-
defp check_unrecognized_properties(%{schema: %{additionalProperties: ap}}, _expected_keys)
55-
when ap != false do
53+
defp check_unrecognized_properties(%{schema: %{additionalProperties: ap}}) when ap != false do
5654
:ok
5755
end
5856

59-
defp check_unrecognized_properties(%{value: value} = ctx, expected_keys) do
57+
defp check_unrecognized_properties(%{value: value, schema: schema} = ctx) do
58+
schema_properties = schema.properties || %{}
6059
input_keys = value |> Map.keys() |> Enum.map(&to_string/1)
61-
schema_keys = expected_keys |> Map.keys() |> Enum.map(&to_string/1)
60+
schema_keys = schema_properties |> Map.keys() |> Enum.map(&to_string/1)
6261
extra_keys = input_keys -- schema_keys
6362

6463
if extra_keys == [] do
@@ -96,20 +95,25 @@ defmodule OpenApiSpex.Cast.Object do
9695

9796
defp check_min_properties(_ctx), do: :ok
9897

99-
defp cast_atom_keys(input_map, properties) do
100-
Enum.reduce(properties, %{}, fn {key, _}, output ->
101-
string_key = to_string(key)
98+
defp cast_atom_keys(%{value: input_map, schema: %{properties: properties}} = ctx) do
99+
value =
100+
Enum.reduce(properties, input_map, fn {key, _}, output ->
101+
string_key = to_string(key)
102102

103-
case input_map do
104-
%{^key => value} -> Map.put(output, key, value)
105-
%{^string_key => value} -> Map.put(output, key, value)
106-
_ -> output
107-
end
108-
end)
103+
if Map.has_key?(output, string_key) do
104+
{value, output} = Map.pop!(output, string_key)
105+
Map.put(output, key, value)
106+
else
107+
output
108+
end
109+
end)
110+
111+
%{ctx | value: value}
109112
end
110113

111-
defp cast_properties(%{value: object, schema: schema_properties} = ctx) do
112-
Enum.reduce(object, {%{}, []}, fn {key, value}, {output, object_errors} ->
114+
defp cast_properties(%{value: object, schema: %{properties: schema_properties}} = ctx) do
115+
object
116+
|> Enum.reduce({%{}, []}, fn {key, value}, {output, object_errors} ->
113117
case cast_property(%{ctx | key: key, value: value, schema: schema_properties}, output) do
114118
{:ok, output} ->
115119
{output, object_errors}
@@ -119,16 +123,16 @@ defmodule OpenApiSpex.Cast.Object do
119123
end
120124
end)
121125
|> case do
122-
{output, []} ->
123-
{:ok, output}
126+
{value, []} ->
127+
{:ok, %{ctx | value: value}}
124128

125129
{_, errors} ->
126130
{:error, errors}
127131
end
128132
end
129133

130-
defp cast_additional_properties(%{schema: %{additionalProperties: ap}} = ctx, original_value) do
131-
original_value
134+
defp cast_additional_properties(%{value: value, schema: %{additionalProperties: ap}} = ctx) do
135+
value
132136
|> get_additional_properties(ctx)
133137
|> Enum.reduce({:ok, ctx}, fn
134138
{key, value}, {:ok, ctx} ->
@@ -140,15 +144,14 @@ defmodule OpenApiSpex.Cast.Object do
140144
end)
141145
end
142146

143-
defp get_additional_properties(original_value, ctx) do
147+
defp get_additional_properties(value, ctx) do
144148
recognized_keys =
145149
(ctx.schema.properties || %{})
146150
|> Map.keys()
147151
|> Enum.flat_map(&[&1, to_string(&1)])
148152
|> MapSet.new()
149153

150-
for {key, _value} = prop <- ensure_not_struct(original_value),
151-
not MapSet.member?(recognized_keys, key) do
154+
for {key, _value} = prop <- value, not MapSet.member?(recognized_keys, key) do
152155
prop
153156
end
154157
end
@@ -172,8 +175,15 @@ defmodule OpenApiSpex.Cast.Object do
172175
end
173176
end
174177

175-
defp apply_defaults(object_value, schema_properties) do
176-
Enum.reduce(schema_properties, object_value, &apply_default/2)
178+
defp apply_defaults(%{opts: opts, value: value, schema: %{properties: properties}} = ctx) do
179+
value =
180+
if Keyword.get(opts, :apply_defaults, true) do
181+
Enum.reduce(properties, value, &apply_default/2)
182+
else
183+
value
184+
end
185+
186+
%{ctx | value: value}
177187
end
178188

179189
defp apply_default({_key, %{default: nil}}, object_value), do: object_value
@@ -188,16 +198,21 @@ defmodule OpenApiSpex.Cast.Object do
188198

189199
defp apply_default(_, object_value), do: object_value
190200

191-
defp to_struct(%{value: value = %_{}}, _original_value), do: value
201+
defp to_struct(%{value: value = %_{}}), do: value
202+
203+
defp to_struct(%{value: value, schema: %{"x-struct": module}}) when not is_nil(module),
204+
do: struct(module, value)
192205

193-
defp to_struct(%{value: value, schema: %{"x-struct": module}}, _)
194-
when not is_nil(module),
195-
do: struct(module, value)
206+
defp to_struct(%{value: value}), do: value
196207

197-
defp to_struct(%{value: value}, %original_module{}), do: struct(original_module, value)
208+
defp handle_struct_value(%{value: %_{} = value, schema: %{"x-struct": module}} = ctx)
209+
when not is_nil(module) do
210+
%{ctx | value: Map.from_struct(value)}
211+
end
198212

199-
defp to_struct(%{value: value}, _original_value), do: value
213+
defp handle_struct_value(%{value: %struct{} = value, schema: schema} = ctx) do
214+
%{ctx | value: Map.from_struct(value), schema: %{schema | "x-struct": struct}}
215+
end
200216

201-
defp ensure_not_struct(val) when is_struct(val), do: Map.from_struct(val)
202-
defp ensure_not_struct(val), do: val
217+
defp handle_struct_value(ctx), do: ctx
203218
end

lib/open_api_spex/cast/utils.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ defmodule OpenApiSpex.Cast.Utils do
1717
def check_required_fields(%{value: input_map} = ctx), do: check_required_fields(ctx, input_map)
1818

1919
def check_required_fields(ctx, %{} = input_map) do
20-
required = ctx.schema.required || []
20+
required = Map.get(ctx.schema, :required) || []
2121

2222
# Adjust required fields list, based on read_write_scope
2323
required =

test/cast_test.exs

+20
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,26 @@ defmodule OpenApiSpec.CastTest do
253253
end
254254
end
255255

256+
describe "opts" do
257+
test "read_write_scope" do
258+
schema = %Schema{
259+
type: :object,
260+
properties: %{
261+
id: %Schema{type: :string, readOnly: true},
262+
name: %Reference{"$ref": "#/components/schemas/Name"},
263+
age: %Schema{type: :integer}
264+
},
265+
required: [:id, :name, :age]
266+
}
267+
268+
schemas = %{"Name" => %Schema{type: :string, readOnly: true}}
269+
270+
value = %{"age" => 30}
271+
assert {:error, _} = Cast.cast(schema, value, schemas, [])
272+
assert {:ok, %{age: 30}} == Cast.cast(schema, value, schemas, read_write_scope: :write)
273+
end
274+
end
275+
256276
describe "ok/1" do
257277
test "basics" do
258278
assert {:ok, 1} = Cast.ok(%Cast{value: 1})

0 commit comments

Comments
 (0)