Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e53c3ff6bb | |||
| a38d007012 |
@@ -115,29 +115,13 @@ given string value.
|
|||||||
|
|
||||||
In the test schema, we can see the following definition:
|
In the test schema, we can see the following definition:
|
||||||
```json
|
```json
|
||||||
"properties": {
|
"Pubkey": {
|
||||||
"from": {
|
"type": "string",
|
||||||
"allOf": [
|
"x-serialization": {
|
||||||
{ "$ref": "#/components/schemas/Pubkey" },
|
"tags": ["ak", "ct"]
|
||||||
{ "x-serialization": {
|
}
|
||||||
"tags": ["ak"]
|
|
||||||
}}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
...
|
|
||||||
"Pubkey": {
|
|
||||||
"type": "string",
|
|
||||||
"x-serialization": {
|
|
||||||
"tags": ["ak", "ct"]
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Whenever the validator encounters an `x-...` property mapped to a validator fun,
|
Whenever the validator encounters an `x-...` property mapped to a validator fun,
|
||||||
this fun is called with the value and the schema part of the property. The return
|
this fun is called with the value and the schema part of the property. The return
|
||||||
value of the fun is ignored, and any normal return is treated as a validation success.
|
value of the fun is ignored, and any normal return is treated as a validation success.
|
||||||
|
|
||||||
The example illustrates a common pattern in OpenAPI specs, where entity references are
|
|
||||||
used extensively. The `Pubkey` data type can have a more general `x-serialization`
|
|
||||||
definition, where multiple key types are accepted, whereas a specialized use of the
|
|
||||||
type can narrow the scope by accepting only a subset of the possible types.
|
|
||||||
|
|||||||
+18
-116
@@ -97,57 +97,14 @@ use_schema(Schema, RootSchema) ->
|
|||||||
normalize() ->
|
normalize() ->
|
||||||
normalize(get_schema()).
|
normalize(get_schema()).
|
||||||
|
|
||||||
normalize(Schema) ->
|
normalize(S) when is_map(S) ->
|
||||||
Schema1 = normalize_map_keys(Schema),
|
#{bin_key(K) => normalize(V) || K := V <- S};
|
||||||
normalize_values(Schema1).
|
normalize(S) when is_list(S) ->
|
||||||
|
[normalize(Sx) || Sx <- S];
|
||||||
normalize_map_keys(S) when is_map(S) ->
|
normalize(S) ->
|
||||||
#{bin_key(K) => normalize_map_keys(V) || K := V <- S};
|
|
||||||
normalize_map_keys(L) when is_list(L) ->
|
|
||||||
[normalize_map_keys(S) || S <- L];
|
|
||||||
normalize_map_keys(S) ->
|
|
||||||
S.
|
S.
|
||||||
|
|
||||||
normalize_values(S) when is_map(S) ->
|
|
||||||
#{K => normalize_value(K, V) || K := V <- S};
|
|
||||||
normalize_values(L) when is_list(L) ->
|
|
||||||
[normalize_values(S) || S <- L];
|
|
||||||
normalize_values(S) ->
|
|
||||||
S.
|
|
||||||
|
|
||||||
normalize_value(<<"type">>, [C|_] = T) when is_integer(C) ->
|
|
||||||
bin_key(T);
|
|
||||||
normalize_value(K, L) when is_list(L) ->
|
|
||||||
%% In some cases, the spec tells us what to do
|
|
||||||
if K == <<"allOf">>; %% 10.2.1.1
|
|
||||||
K == <<"anyOf">>; %% 10.2.1.2
|
|
||||||
K == <<"oneOf">>; %% 10.2.1.3
|
|
||||||
K == <<"prefixItems">> -> %% 10.3.1.1
|
|
||||||
%% These MUST refer to arrays
|
|
||||||
[normalize_values(S) || S <- L];
|
|
||||||
K == <<"contains">> ->
|
|
||||||
%% 10.3.1.3 Value MUST be a valid schema
|
|
||||||
normalize_values(L);
|
|
||||||
true ->
|
|
||||||
try unicode:characters_to_binary(L)
|
|
||||||
catch
|
|
||||||
error:_ ->
|
|
||||||
[normalize_values(S) || S <- L]
|
|
||||||
end
|
|
||||||
end;
|
|
||||||
normalize_value(_, V) when is_atom(V) ->
|
|
||||||
atom_to_binary(V, utf8);
|
|
||||||
normalize_value(_, V) when is_list(V) ->
|
|
||||||
try unicode:characters_to_binary(V)
|
|
||||||
catch
|
|
||||||
error:_ ->
|
|
||||||
[normalize_values(S) || S <- V]
|
|
||||||
end;
|
|
||||||
normalize_value(_, V) ->
|
|
||||||
V.
|
|
||||||
|
|
||||||
bin_key(A) when is_atom(A) -> atom_to_binary(A, utf8);
|
bin_key(A) when is_atom(A) -> atom_to_binary(A, utf8);
|
||||||
bin_key(L) when is_list(L) -> unicode:characters_to_binary(L);
|
|
||||||
bin_key(B) when is_binary(B) -> B.
|
bin_key(B) when is_binary(B) -> B.
|
||||||
|
|
||||||
clear() ->
|
clear() ->
|
||||||
@@ -202,10 +159,10 @@ any_schema_prop(P, S0, [S|Ss]) ->
|
|||||||
any_schema_prop(P, S, []) ->
|
any_schema_prop(P, S, []) ->
|
||||||
schema_prop_find(P, S).
|
schema_prop_find(P, S).
|
||||||
|
|
||||||
schema_prop_find(P, #st{s = S} = St) when is_map(S) ->
|
schema_prop_find(P, #st{s = S, r = RS}) when is_map(S) ->
|
||||||
case maps:find(P, S) of
|
case maps:find(P, S) of
|
||||||
{ok, #{<<"$ref">> := Sub} = M} when map_size(M) == 1 ->
|
{ok, #{<<"$ref">> := Sub} = M} when map_size(M) == 1 ->
|
||||||
D = expand_ref(Sub, St),
|
D = expand_ref(Sub, RS),
|
||||||
{ok, D};
|
{ok, D};
|
||||||
Other -> Other
|
Other -> Other
|
||||||
end;
|
end;
|
||||||
@@ -279,8 +236,8 @@ get_type(#st{} = St0, Value) ->
|
|||||||
|
|
||||||
get_type(#st{} = St, Ss, Value) ->
|
get_type(#st{} = St, Ss, Value) ->
|
||||||
case any_schema_prop(<<"type">>, St, Ss) of
|
case any_schema_prop(<<"type">>, St, Ss) of
|
||||||
{ok, Type} when is_binary(Type); is_list(Type) ->
|
{ok, TBin} ->
|
||||||
select_type(Type, Value, St);
|
select_type(TBin, Value, St);
|
||||||
error ->
|
error ->
|
||||||
try infer_type(Value)
|
try infer_type(Value)
|
||||||
catch
|
catch
|
||||||
@@ -400,7 +357,7 @@ convert_enums(V, St0) when is_binary(V) ->
|
|||||||
{Ss, St1} = schemas_from_dynamic_eval(V, St),
|
{Ss, St1} = schemas_from_dynamic_eval(V, St),
|
||||||
case any_schema_prop(<<"enum">>, St1, Ss) of
|
case any_schema_prop(<<"enum">>, St1, Ss) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
binary_to_atom(V, unicode);
|
binary_to_atom(V, utf8);
|
||||||
_ ->
|
_ ->
|
||||||
V
|
V
|
||||||
end;
|
end;
|
||||||
@@ -818,8 +775,8 @@ any_pattern_({Pat, Schema, I}, P) ->
|
|||||||
|
|
||||||
maybe_expand_ref(#st{s = S} = St) ->
|
maybe_expand_ref(#st{s = S} = St) ->
|
||||||
case S of
|
case S of
|
||||||
#{<<"$ref">> := Ref} ->
|
#{<<"$ref">> := Ref} = R when map_size(R) == 1 ->
|
||||||
St#st{s = expand_ref(Ref, St)};
|
St#st{s = expand_ref(Ref, St#st.r)};
|
||||||
_ ->
|
_ ->
|
||||||
St
|
St
|
||||||
end.
|
end.
|
||||||
@@ -977,7 +934,7 @@ expand_schema(S) ->
|
|||||||
%% S#{<<"definitions">> := expand_schema(D, S)}.
|
%% S#{<<"definitions">> := expand_schema(D, S)}.
|
||||||
|
|
||||||
expand_schema(#{<<"$ref">> := Path} = V, S0) when map_size(V) == 1 ->
|
expand_schema(#{<<"$ref">> := Path} = V, S0) when map_size(V) == 1 ->
|
||||||
expand_schema(expand_ref(Path, use_schema(S0)), S0);
|
expand_schema(expand_ref(Path, S0), S0);
|
||||||
expand_schema(S, S0) when is_map(S) ->
|
expand_schema(S, S0) when is_map(S) ->
|
||||||
%% https://json-schema.org/understanding-json-schema/structuring#dollarref
|
%% https://json-schema.org/understanding-json-schema/structuring#dollarref
|
||||||
%% When $id is used in a subschema, it indicates an embedded schema.
|
%% When $id is used in a subschema, it indicates an embedded schema.
|
||||||
@@ -1002,7 +959,7 @@ expand_schema(S, _) ->
|
|||||||
S.
|
S.
|
||||||
|
|
||||||
expand_schema_(K, #{<<"$ref">> := Path} = V, Acc, S0) when map_size(V) == 1 ->
|
expand_schema_(K, #{<<"$ref">> := Path} = V, Acc, S0) when map_size(V) == 1 ->
|
||||||
D = expand_ref(Path, use_schema(S0)),
|
D = expand_ref(Path, S0),
|
||||||
Acc#{K => D};
|
Acc#{K => D};
|
||||||
expand_schema_(K, V, Acc, S0) ->
|
expand_schema_(K, V, Acc, S0) ->
|
||||||
Acc#{K => expand_schema(V, S0)}.
|
Acc#{K => expand_schema(V, S0)}.
|
||||||
@@ -1010,13 +967,13 @@ expand_schema_(K, V, Acc, S0) ->
|
|||||||
expand_ref(R, _, #{follow_refs := false}) ->
|
expand_ref(R, _, #{follow_refs := false}) ->
|
||||||
R;
|
R;
|
||||||
expand_ref(R, S, _) ->
|
expand_ref(R, S, _) ->
|
||||||
expand_ref(R, use_schema(S)).
|
expand_ref(R, S).
|
||||||
|
|
||||||
expand_ref(<<"#">>, #st{r = R}) ->
|
expand_ref(<<"#">>, S) ->
|
||||||
%% The $ref keyword may be used to create recursive schemas that refer to themselves.
|
%% The $ref keyword may be used to create recursive schemas that refer to themselves.
|
||||||
%% This done by using `{"$ref" : "#"}`
|
%% This done by using `{"$ref" : "#"}`
|
||||||
R;
|
S;
|
||||||
expand_ref(<<"#/", Path/binary>>, #st{r = S}) ->
|
expand_ref(<<"#/", Path/binary>>, S) ->
|
||||||
Key = filename:split(Path),
|
Key = filename:split(Path),
|
||||||
case schema(Key, S, #{follow_refs => false}) of
|
case schema(Key, S, #{follow_refs => false}) of
|
||||||
{ok, #{<<"$ref">> := _}} ->
|
{ok, #{<<"$ref">> := _}} ->
|
||||||
@@ -1036,63 +993,8 @@ expand_ref(<<"#/", Path/binary>>, #st{r = S}) ->
|
|||||||
Def;
|
Def;
|
||||||
undefined ->
|
undefined ->
|
||||||
error(unknown_ref, [Path])
|
error(unknown_ref, [Path])
|
||||||
end;
|
|
||||||
expand_ref(<<"#", Anchor/binary>>, #st{r = S}) ->
|
|
||||||
case find_anchor(Anchor, S) of
|
|
||||||
{ok, Ss} ->
|
|
||||||
Ss;
|
|
||||||
error ->
|
|
||||||
error({unknown_anchor, Anchor})
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% get_schema_by_path([T|P], #{<<"type">> := Ts} = S) when is_atom(T) ->
|
|
||||||
%% case atom_to_binary(T, utf8) of
|
|
||||||
%% Ts ->
|
|
||||||
%% get_schema_by_path(P, S);
|
|
||||||
%% Prop when is_map_key(Prop, S) ->
|
|
||||||
%% get_schema_by_path(P, maps:get(Prop, S));
|
|
||||||
%% _ ->
|
|
||||||
%% error(invalid_schema_path)
|
|
||||||
%% end;
|
|
||||||
%% get_schema_by_path([Property|P], #{<<"properties">> := Ps} = S) when is_binary(Property) ->
|
|
||||||
%% get_schema_by_path(P, maps:get(Property, Ps));
|
|
||||||
%% get_schema_by_path([], S) ->
|
|
||||||
%% S.
|
|
||||||
|
|
||||||
%% == Anchor search (unoptimized - must search whole root schema)
|
|
||||||
|
|
||||||
find_anchor(Anchor, S) when map_get(<<"$anchor">>, S) =:= Anchor ->
|
|
||||||
{ok, S};
|
|
||||||
find_anchor(Anchor, S) when is_map(S) ->
|
|
||||||
Iter = maps:iterator(S),
|
|
||||||
map_search_anchor(maps:next(Iter), Anchor);
|
|
||||||
find_anchor(Anchor, S) when is_list(S) ->
|
|
||||||
list_search_anchor(S, Anchor);
|
|
||||||
find_anchor(_, _) ->
|
|
||||||
error.
|
|
||||||
|
|
||||||
map_search_anchor({_K, V, I}, Anchor) ->
|
|
||||||
case find_anchor(Anchor, V) of
|
|
||||||
{ok, _} = Ok ->
|
|
||||||
Ok;
|
|
||||||
error ->
|
|
||||||
map_search_anchor(maps:next(I), Anchor)
|
|
||||||
end;
|
|
||||||
map_search_anchor(none, _) ->
|
|
||||||
error.
|
|
||||||
|
|
||||||
list_search_anchor([H | T], Anchor) ->
|
|
||||||
case find_anchor(Anchor, H) of
|
|
||||||
{ok, _} = Ok ->
|
|
||||||
Ok;
|
|
||||||
error ->
|
|
||||||
list_search_anchor(T, Anchor)
|
|
||||||
end;
|
|
||||||
list_search_anchor([], _) ->
|
|
||||||
error.
|
|
||||||
|
|
||||||
%% ==
|
|
||||||
|
|
||||||
schema(Path) ->
|
schema(Path) ->
|
||||||
schema(Path, get_schema()).
|
schema(Path, get_schema()).
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ schema_spec_examples_test_() ->
|
|||||||
?t(t_ref_loop())
|
?t(t_ref_loop())
|
||||||
, ?t(t_recursive_def())
|
, ?t(t_recursive_def())
|
||||||
, ?t(t_nested_refs())
|
, ?t(t_nested_refs())
|
||||||
, ?t(t_anchors())
|
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
array() -> #{<<"type">> => <<"array">>}.
|
array() -> #{<<"type">> => <<"array">>}.
|
||||||
@@ -294,8 +293,7 @@ fails(V, S, Opts, Reason) when is_atom(Reason) ->
|
|||||||
fails(V, S, Opts, #{e => Reason});
|
fails(V, S, Opts, #{e => Reason});
|
||||||
fails(V, S, Opts, Expect) ->
|
fails(V, S, Opts, Expect) ->
|
||||||
try validate(V, S, Opts) of
|
try validate(V, S, Opts) of
|
||||||
Other ->
|
_ ->
|
||||||
?debugFmt("Expected failure, Other = ~p", [Other]),
|
|
||||||
error({expected_exception, #{v => V,
|
error({expected_exception, #{v => V,
|
||||||
s => S,
|
s => S,
|
||||||
e => Expect}})
|
e => Expect}})
|
||||||
@@ -305,8 +303,6 @@ fails(V, S, Opts, Expect) ->
|
|||||||
end.
|
end.
|
||||||
%% ?assertError({Reason, [], V}, valid(V, S)).
|
%% ?assertError({Reason, [], V}, valid(V, S)).
|
||||||
|
|
||||||
match_expected('_', _) ->
|
|
||||||
ok;
|
|
||||||
match_expected(E, R) ->
|
match_expected(E, R) ->
|
||||||
case maps:fold(
|
case maps:fold(
|
||||||
fun(K, V, Acc) ->
|
fun(K, V, Acc) ->
|
||||||
@@ -354,6 +350,7 @@ all_fail(Vs, S, Reason) ->
|
|||||||
read(F) ->
|
read(F) ->
|
||||||
FullF = filename:join(
|
FullF = filename:join(
|
||||||
filename:dirname(code:which(?MODULE)), F),
|
filename:dirname(code:which(?MODULE)), F),
|
||||||
|
?debugFmt("FullF = ~s~n", [FullF]),
|
||||||
{ok, Bin} = file:read_file(FullF),
|
{ok, Bin} = file:read_file(FullF),
|
||||||
dec(Bin).
|
dec(Bin).
|
||||||
|
|
||||||
@@ -405,11 +402,3 @@ t_nested_refs() ->
|
|||||||
validate(Vs, S, Opts),
|
validate(Vs, S, Opts),
|
||||||
fails(Vf, S, Opts, #{e => failing_schemas}),
|
fails(Vf, S, Opts, #{e => failing_schemas}),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_anchors() ->
|
|
||||||
S = read("data/anchors.json"),
|
|
||||||
validate(#{<<"person">> => #{ <<"name">> => <<"Ulf">>
|
|
||||||
, <<"age">> => 29 }}, S, #{}),
|
|
||||||
fails(#{<<"person">> => #{ <<"name">> => <<"Ulf">>
|
|
||||||
, <<"age">> => -17 }}, S, #{}, not_in_range),
|
|
||||||
ok.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user