Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25f81ae339 |
@@ -25,6 +25,19 @@ is still incomplete. `"definitions"` and `"$ref"` properties are recognized.
|
||||
As almost anything is theoretically possible with JSON-Schema,
|
||||
there are surely other things that are unsupported.
|
||||
|
||||
### Schema dialects
|
||||
|
||||
If a schema declares `"$schema"`, the validator recognizes draft-04,
|
||||
draft-06, draft-07, 2019-09 and 2020-12. It rejects unknown dialects and
|
||||
known constructs that do not belong to the declared draft. This currently
|
||||
includes boolean-schema availability, keyword introduction dates,
|
||||
draft-specific exclusive-bound syntax and the 2020-12 array vocabulary.
|
||||
|
||||
Schemas without `"$schema"` retain the historical compatibility behavior,
|
||||
which accepts the supported keyword subset without assigning it to one
|
||||
particular JSON-Schema draft. Dialect recognition does not yet imply full
|
||||
conformance with every keyword in that draft.
|
||||
|
||||
### All standard data types
|
||||
|
||||
* `"null"`
|
||||
|
||||
+336
-126
@@ -46,6 +46,13 @@
|
||||
|
||||
-type schema() :: json().
|
||||
|
||||
-type draft() :: compatibility
|
||||
| draft4
|
||||
| draft6
|
||||
| draft7
|
||||
| draft2019_09
|
||||
| draft2020_12.
|
||||
|
||||
-type ext_fun() :: fun( (json(), schema()) -> any() | no_return() ).
|
||||
-type extensions() :: #{ binary() => ext_fun() }.
|
||||
-type options() :: #{coerce => boolean(),
|
||||
@@ -54,6 +61,7 @@
|
||||
|
||||
-record(st, { s :: schema() %% schema
|
||||
, r :: schema() %% root schema
|
||||
, draft = compatibility :: draft()
|
||||
, p = []
|
||||
, a = [] %% annotations
|
||||
, v :: json() | undefined %% value
|
||||
@@ -89,67 +97,311 @@ get_schema(Default) ->
|
||||
|
||||
-spec use_schema(schema() | st()) -> st().
|
||||
use_schema(#st{} = St) -> St;
|
||||
use_schema(S) -> #st{s = S, r = S}.
|
||||
use_schema(S) -> new_state(S, S).
|
||||
|
||||
use_schema(Schema, RootSchema) ->
|
||||
#st{s = Schema, r = RootSchema}.
|
||||
new_state(Schema, RootSchema).
|
||||
|
||||
normalize() ->
|
||||
normalize(get_schema()).
|
||||
|
||||
normalize(Schema) ->
|
||||
Schema1 = normalize_map_keys(Schema),
|
||||
normalize_values(Schema1).
|
||||
|
||||
normalize_map_keys(S) when is_map(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) ->
|
||||
normalize(S) when is_map(S) ->
|
||||
#{bin_key(K) => normalize(V) || K := V <- S};
|
||||
normalize(S) when is_list(S) ->
|
||||
[normalize(Sx) || Sx <- S];
|
||||
normalize(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(L) when is_list(L) -> unicode:characters_to_binary(L);
|
||||
bin_key(B) when is_binary(B) -> B.
|
||||
|
||||
new_state(Schema, RootSchema) ->
|
||||
Draft = schema_draft(RootSchema, compatibility, []),
|
||||
case Draft of
|
||||
compatibility -> ok;
|
||||
_ -> check_schema(normalize(RootSchema), Draft, [])
|
||||
end,
|
||||
#st{s = Schema, r = RootSchema, draft = Draft}.
|
||||
|
||||
schema_draft(Schema, Inherited, Path) when is_map(Schema) ->
|
||||
case schema_value(<<"$schema">>, '$schema', Schema) of
|
||||
undefined ->
|
||||
Inherited;
|
||||
URI ->
|
||||
draft_from_uri(URI, Path ++ [<<"$schema">>])
|
||||
end;
|
||||
schema_draft(_, Inherited, _) ->
|
||||
Inherited.
|
||||
|
||||
schema_value(BinaryKey, AtomKey, Schema) ->
|
||||
case maps:find(BinaryKey, Schema) of
|
||||
{ok, Value} -> Value;
|
||||
error -> maps:get(AtomKey, Schema, undefined)
|
||||
end.
|
||||
|
||||
draft_from_uri(URI, _) when URI =:= <<"http://json-schema.org/draft-04/schema#">>;
|
||||
URI =:= <<"https://json-schema.org/draft-04/schema#">>;
|
||||
URI =:= <<"http://json-schema.org/draft-04/schema">>;
|
||||
URI =:= <<"https://json-schema.org/draft-04/schema">> ->
|
||||
draft4;
|
||||
draft_from_uri(URI, _) when URI =:= <<"http://json-schema.org/draft-06/schema#">>;
|
||||
URI =:= <<"https://json-schema.org/draft-06/schema#">>;
|
||||
URI =:= <<"http://json-schema.org/draft-06/schema">>;
|
||||
URI =:= <<"https://json-schema.org/draft-06/schema">> ->
|
||||
draft6;
|
||||
draft_from_uri(URI, _) when URI =:= <<"http://json-schema.org/draft-07/schema#">>;
|
||||
URI =:= <<"https://json-schema.org/draft-07/schema#">>;
|
||||
URI =:= <<"http://json-schema.org/draft-07/schema">>;
|
||||
URI =:= <<"https://json-schema.org/draft-07/schema">> ->
|
||||
draft7;
|
||||
draft_from_uri(URI, _) when URI =:= <<"http://json-schema.org/draft/2019-09/schema#">>;
|
||||
URI =:= <<"https://json-schema.org/draft/2019-09/schema#">>;
|
||||
URI =:= <<"http://json-schema.org/draft/2019-09/schema">>;
|
||||
URI =:= <<"https://json-schema.org/draft/2019-09/schema">> ->
|
||||
draft2019_09;
|
||||
draft_from_uri(URI, _) when URI =:= <<"http://json-schema.org/draft/2020-12/schema#">>;
|
||||
URI =:= <<"https://json-schema.org/draft/2020-12/schema#">>;
|
||||
URI =:= <<"http://json-schema.org/draft/2020-12/schema">>;
|
||||
URI =:= <<"https://json-schema.org/draft/2020-12/schema">> ->
|
||||
draft2020_12;
|
||||
draft_from_uri(URI, Path) ->
|
||||
schema_error(unsupported_schema_draft,
|
||||
#{draft_uri => URI, p => Path}).
|
||||
|
||||
check_schema(_, compatibility, _) ->
|
||||
ok;
|
||||
check_schema(Schema, draft4 = Draft, Path) when is_boolean(Schema) ->
|
||||
case lists:reverse(Path) of
|
||||
[<<"additionalProperties">> | _] -> ok;
|
||||
[<<"additionalItems">> | _] -> ok;
|
||||
_ ->
|
||||
schema_error(boolean_schema_not_in_draft,
|
||||
#{draft => Draft, p => Path})
|
||||
end;
|
||||
check_schema(Schema, _Draft, _Path) when is_boolean(Schema) ->
|
||||
ok;
|
||||
check_schema(Schema, Draft0, Path) when is_map(Schema) ->
|
||||
Draft = schema_draft(Schema, Draft0, Path),
|
||||
maps:foreach(
|
||||
fun(Keyword0, Value) ->
|
||||
Keyword = bin_key(Keyword0),
|
||||
check_keyword(Keyword, Value, Draft, Path ++ [Keyword])
|
||||
end, Schema),
|
||||
check_schema_shape(Schema, Draft, Path),
|
||||
check_subschemas(Schema, Draft, Path);
|
||||
check_schema(_, Draft, Path) ->
|
||||
schema_error(invalid_schema,
|
||||
#{draft => Draft, p => Path}).
|
||||
|
||||
check_keyword(Keyword, Value, Draft, Path) ->
|
||||
case minimum_draft(Keyword) of
|
||||
undefined -> ok;
|
||||
Minimum ->
|
||||
case draft_rank(Draft) >= draft_rank(Minimum) of
|
||||
true -> ok;
|
||||
false ->
|
||||
schema_error(keyword_not_in_draft,
|
||||
#{draft => Draft,
|
||||
keyword => Keyword,
|
||||
introduced_in => Minimum,
|
||||
p => Path})
|
||||
end
|
||||
end,
|
||||
case maximum_draft(Keyword) of
|
||||
undefined -> ok;
|
||||
Maximum ->
|
||||
case draft_rank(Draft) =< draft_rank(Maximum) of
|
||||
true -> ok;
|
||||
false ->
|
||||
schema_error(keyword_not_in_draft,
|
||||
#{draft => Draft,
|
||||
keyword => Keyword,
|
||||
last_supported_in => Maximum,
|
||||
p => Path})
|
||||
end
|
||||
end,
|
||||
check_keyword_shape(Keyword, Value, Draft, Path).
|
||||
|
||||
minimum_draft(<<"$id">>) -> draft6;
|
||||
minimum_draft(<<"const">>) -> draft6;
|
||||
minimum_draft(<<"contains">>) -> draft6;
|
||||
minimum_draft(<<"propertyNames">>) -> draft6;
|
||||
minimum_draft(<<"examples">>) -> draft6;
|
||||
minimum_draft(<<"$comment">>) -> draft7;
|
||||
minimum_draft(<<"if">>) -> draft7;
|
||||
minimum_draft(<<"then">>) -> draft7;
|
||||
minimum_draft(<<"else">>) -> draft7;
|
||||
minimum_draft(<<"writeOnly">>) -> draft7;
|
||||
minimum_draft(<<"contentMediaType">>) -> draft7;
|
||||
minimum_draft(<<"contentEncoding">>) -> draft7;
|
||||
minimum_draft(<<"$defs">>) -> draft2019_09;
|
||||
minimum_draft(<<"$anchor">>) -> draft2019_09;
|
||||
minimum_draft(<<"$recursiveAnchor">>) -> draft2019_09;
|
||||
minimum_draft(<<"$recursiveRef">>) -> draft2019_09;
|
||||
minimum_draft(<<"dependentRequired">>) -> draft2019_09;
|
||||
minimum_draft(<<"dependentSchemas">>) -> draft2019_09;
|
||||
minimum_draft(<<"unevaluatedItems">>) -> draft2019_09;
|
||||
minimum_draft(<<"unevaluatedProperties">>) -> draft2019_09;
|
||||
minimum_draft(<<"minContains">>) -> draft2019_09;
|
||||
minimum_draft(<<"maxContains">>) -> draft2019_09;
|
||||
minimum_draft(<<"contentSchema">>) -> draft2019_09;
|
||||
minimum_draft(<<"$dynamicAnchor">>) -> draft2020_12;
|
||||
minimum_draft(<<"$dynamicRef">>) -> draft2020_12;
|
||||
minimum_draft(<<"prefixItems">>) -> draft2020_12;
|
||||
minimum_draft(_) -> undefined.
|
||||
|
||||
maximum_draft(<<"additionalItems">>) -> draft2019_09;
|
||||
maximum_draft(_) -> undefined.
|
||||
|
||||
draft_rank(draft4) -> 4;
|
||||
draft_rank(draft6) -> 6;
|
||||
draft_rank(draft7) -> 7;
|
||||
draft_rank(draft2019_09) -> 8;
|
||||
draft_rank(draft2020_12) -> 9.
|
||||
|
||||
check_keyword_shape(<<"exclusiveMinimum">>, Value, draft4, Path) ->
|
||||
require_type(fun is_boolean/1, Value, draft4, Path);
|
||||
check_keyword_shape(<<"exclusiveMaximum">>, Value, draft4, Path) ->
|
||||
require_type(fun is_boolean/1, Value, draft4, Path);
|
||||
check_keyword_shape(<<"exclusiveMinimum">>, Value, Draft, Path) ->
|
||||
require_type(fun is_number/1, Value, Draft, Path);
|
||||
check_keyword_shape(<<"exclusiveMaximum">>, Value, Draft, Path) ->
|
||||
require_type(fun is_number/1, Value, Draft, Path);
|
||||
check_keyword_shape(<<"items">>, Value, draft2020_12, Path)
|
||||
when is_list(Value) ->
|
||||
schema_error(keyword_value_not_in_draft,
|
||||
#{draft => draft2020_12,
|
||||
keyword => <<"items">>,
|
||||
p => Path});
|
||||
check_keyword_shape(_, _, _, _) ->
|
||||
ok.
|
||||
|
||||
check_schema_shape(Schema, draft4, Path) ->
|
||||
check_exclusive_bound(Schema, <<"exclusiveMinimum">>, <<"minimum">>,
|
||||
draft4, Path),
|
||||
check_exclusive_bound(Schema, <<"exclusiveMaximum">>, <<"maximum">>,
|
||||
draft4, Path);
|
||||
check_schema_shape(_, _, _) ->
|
||||
ok.
|
||||
|
||||
check_exclusive_bound(Schema, Exclusive, Bound, Draft, Path) ->
|
||||
case {maps:find(Exclusive, Schema), maps:is_key(Bound, Schema)} of
|
||||
{{ok, _}, false} ->
|
||||
schema_error(missing_keyword_for_draft,
|
||||
#{draft => Draft,
|
||||
keyword => Exclusive,
|
||||
required_keyword => Bound,
|
||||
p => Path ++ [Exclusive]});
|
||||
_ ->
|
||||
ok
|
||||
end.
|
||||
|
||||
require_type(Predicate, Value, Draft, Path) ->
|
||||
case Predicate(Value) of
|
||||
true -> ok;
|
||||
false ->
|
||||
schema_error(keyword_value_not_in_draft,
|
||||
#{draft => Draft, p => Path, value => Value})
|
||||
end.
|
||||
|
||||
check_subschemas(Schema, Draft, Path) ->
|
||||
check_schema_values(
|
||||
[<<"additionalProperties">>, <<"additionalItems">>, <<"contains">>,
|
||||
<<"propertyNames">>, <<"not">>, <<"if">>, <<"then">>, <<"else">>,
|
||||
<<"unevaluatedProperties">>, <<"unevaluatedItems">>,
|
||||
<<"contentSchema">>],
|
||||
Schema, Draft, Path),
|
||||
check_schema_arrays(
|
||||
[<<"allOf">>, <<"anyOf">>, <<"oneOf">>, <<"prefixItems">>],
|
||||
Schema, Draft, Path),
|
||||
check_schema_maps(
|
||||
[<<"properties">>, <<"patternProperties">>, <<"definitions">>,
|
||||
<<"$defs">>, <<"dependentSchemas">>],
|
||||
Schema, Draft, Path),
|
||||
check_schema_items(Schema, Draft, Path),
|
||||
check_dependencies(Schema, Draft, Path).
|
||||
|
||||
check_schema_values(Keys, Schema, Draft, Path) ->
|
||||
lists:foreach(
|
||||
fun(Key) ->
|
||||
case maps:find(Key, Schema) of
|
||||
{ok, SubSchema} ->
|
||||
check_schema(SubSchema, Draft, Path ++ [Key]);
|
||||
error ->
|
||||
ok
|
||||
end
|
||||
end, Keys).
|
||||
|
||||
check_schema_arrays(Keys, Schema, Draft, Path) ->
|
||||
lists:foreach(
|
||||
fun(Key) ->
|
||||
case maps:find(Key, Schema) of
|
||||
{ok, SubSchemas} when is_list(SubSchemas) ->
|
||||
check_schema_list(SubSchemas, Draft, Path ++ [Key], 0);
|
||||
{ok, Value} ->
|
||||
schema_error(invalid_schema,
|
||||
#{draft => Draft,
|
||||
keyword => Key,
|
||||
value => Value,
|
||||
p => Path ++ [Key]});
|
||||
error ->
|
||||
ok
|
||||
end
|
||||
end, Keys).
|
||||
|
||||
check_schema_list([Schema | Schemas], Draft, Path, Index) ->
|
||||
check_schema(Schema, Draft, Path ++ [Index]),
|
||||
check_schema_list(Schemas, Draft, Path, Index + 1);
|
||||
check_schema_list([], _, _, _) ->
|
||||
ok.
|
||||
|
||||
check_schema_maps(Keys, Schema, Draft, Path) ->
|
||||
lists:foreach(
|
||||
fun(Key) ->
|
||||
case maps:find(Key, Schema) of
|
||||
{ok, SubSchemas} when is_map(SubSchemas) ->
|
||||
maps:foreach(
|
||||
fun(Name, SubSchema) ->
|
||||
check_schema(SubSchema, Draft,
|
||||
Path ++ [Key, Name])
|
||||
end, SubSchemas);
|
||||
{ok, Value} ->
|
||||
schema_error(invalid_schema,
|
||||
#{draft => Draft,
|
||||
keyword => Key,
|
||||
value => Value,
|
||||
p => Path ++ [Key]});
|
||||
error ->
|
||||
ok
|
||||
end
|
||||
end, Keys).
|
||||
|
||||
check_schema_items(Schema, Draft, Path) ->
|
||||
case maps:find(<<"items">>, Schema) of
|
||||
{ok, Items} when is_list(Items) ->
|
||||
check_schema_list(Items, Draft, Path ++ [<<"items">>], 0);
|
||||
{ok, Items} ->
|
||||
check_schema(Items, Draft, Path ++ [<<"items">>]);
|
||||
error ->
|
||||
ok
|
||||
end.
|
||||
|
||||
check_dependencies(Schema, Draft, Path) ->
|
||||
case maps:find(<<"dependencies">>, Schema) of
|
||||
{ok, Dependencies} when is_map(Dependencies) ->
|
||||
maps:foreach(
|
||||
fun(_, Dependency) when is_list(Dependency) -> ok;
|
||||
(Name, Dependency) ->
|
||||
check_schema(Dependency, Draft,
|
||||
Path ++ [<<"dependencies">>, Name])
|
||||
end, Dependencies);
|
||||
_ ->
|
||||
ok
|
||||
end.
|
||||
|
||||
schema_error(Reason, Details) ->
|
||||
error(Details#{e => Reason}).
|
||||
|
||||
clear() ->
|
||||
persistent_term:erase({?MODULE,'$schema'}),
|
||||
persistent_term:erase({?MODULE,'$config'}),
|
||||
@@ -159,8 +411,7 @@ clear() ->
|
||||
update_config(Cfg) ->
|
||||
OldCfg = get_config(),
|
||||
Schema = get_schema(),
|
||||
Res = merge(Cfg, OldCfg, #st{s = Schema,
|
||||
r = Schema }),
|
||||
Res = merge(Cfg, OldCfg, Schema),
|
||||
set_config(Res),
|
||||
Res.
|
||||
|
||||
@@ -172,7 +423,7 @@ merge(A, B) ->
|
||||
merge(A, B, #st{} = St) ->
|
||||
merge_(A, B, St);
|
||||
merge(A, B, Schema) ->
|
||||
merge_(A, B, #st{s = Schema, r = Schema}).
|
||||
merge_(A, B, new_state(Schema, Schema)).
|
||||
|
||||
%% Neither the JSON spec or the JSON-Schema spec are very helpful
|
||||
%% regarding what takes precedence if dynamically evaluated parts
|
||||
@@ -202,10 +453,10 @@ any_schema_prop(P, S0, [S|Ss]) ->
|
||||
any_schema_prop(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
|
||||
{ok, #{<<"$ref">> := Sub} = M} when map_size(M) == 1 ->
|
||||
D = expand_ref(Sub, St),
|
||||
D = expand_ref(Sub, RS),
|
||||
{ok, D};
|
||||
Other -> Other
|
||||
end;
|
||||
@@ -279,8 +530,8 @@ get_type(#st{} = St0, Value) ->
|
||||
|
||||
get_type(#st{} = St, Ss, Value) ->
|
||||
case any_schema_prop(<<"type">>, St, Ss) of
|
||||
{ok, Type} when is_binary(Type); is_list(Type) ->
|
||||
select_type(Type, Value, St);
|
||||
{ok, TBin} ->
|
||||
select_type(TBin, Value, St);
|
||||
error ->
|
||||
try infer_type(Value)
|
||||
catch
|
||||
@@ -400,7 +651,7 @@ convert_enums(V, St0) when is_binary(V) ->
|
||||
{Ss, St1} = schemas_from_dynamic_eval(V, St),
|
||||
case any_schema_prop(<<"enum">>, St1, Ss) of
|
||||
{ok, _} ->
|
||||
binary_to_atom(V, unicode);
|
||||
binary_to_atom(V, utf8);
|
||||
_ ->
|
||||
V
|
||||
end;
|
||||
@@ -439,7 +690,7 @@ valid(V) ->
|
||||
valid(V, #st{} = St) ->
|
||||
valid_(V, St#st{v = V});
|
||||
valid(V, Schema) ->
|
||||
valid(V, #st{p = [], s = Schema, r = Schema, v = V}).
|
||||
valid(V, (new_state(Schema, Schema))#st{v = V}).
|
||||
|
||||
valid_(V, #st{s = true}) -> V;
|
||||
valid_(_, #st{s = false} = St) -> fail(invalid, St);
|
||||
@@ -614,6 +865,19 @@ valid_number_(I, Sub, St, Ss) when is_number(I) ->
|
||||
fail(not_a_multiple, St1)
|
||||
end
|
||||
end,
|
||||
validate_number_bounds(I, St, Ss),
|
||||
I.
|
||||
|
||||
validate_number_bounds(I, #st{draft = draft4} = St, Ss) ->
|
||||
Min = schema_prop(<<"minimum">>, St, Ss, I),
|
||||
Max = schema_prop(<<"maximum">>, St, Ss, I),
|
||||
EMin = schema_prop(<<"exclusiveMinimum">>, St, Ss, false),
|
||||
EMax = schema_prop(<<"exclusiveMaximum">>, St, Ss, false),
|
||||
MinOp = case EMin of true -> '<'; false -> '=<' end,
|
||||
MaxOp = case EMax of true -> '>'; false -> '>=' end,
|
||||
test_range(MaxOp, Max, I, add_anno(Max, push_path(maximum, St))),
|
||||
test_range(MinOp, Min, I, add_anno(Min, push_path(minimum, St)));
|
||||
validate_number_bounds(I, St, Ss) ->
|
||||
Min = schema_prop(<<"minimum">>, St, Ss, I),
|
||||
Max = schema_prop(<<"maximum">>, St, Ss, I),
|
||||
test_range('>=', Max, I, add_anno(Max, push_path(maximum, St))),
|
||||
@@ -621,8 +885,7 @@ valid_number_(I, Sub, St, Ss) when is_number(I) ->
|
||||
EMin = schema_prop(<<"exclusiveMinimum">>, St, Ss, I-1),
|
||||
EMax = schema_prop(<<"exclusiveMaximum">>, St, Ss, I+1),
|
||||
test_range('>', EMax, I, add_anno(EMax, push_path(exclusiveMaximum, St))),
|
||||
test_range('<', EMin, I, add_anno(EMin, push_path(exclusiveMinimum, St))),
|
||||
I.
|
||||
test_range('<', EMin, I, add_anno(EMin, push_path(exclusiveMinimum, St))).
|
||||
|
||||
coerce_num(integer, I) when is_binary(I) ->
|
||||
binary_to_integer(I);
|
||||
@@ -818,8 +1081,8 @@ any_pattern_({Pat, Schema, I}, P) ->
|
||||
|
||||
maybe_expand_ref(#st{s = S} = St) ->
|
||||
case S of
|
||||
#{<<"$ref">> := Ref} ->
|
||||
St#st{s = expand_ref(Ref, St)};
|
||||
#{<<"$ref">> := Ref} = R when map_size(R) == 1 ->
|
||||
St#st{s = expand_ref(Ref, St#st.r)};
|
||||
_ ->
|
||||
St
|
||||
end.
|
||||
@@ -915,7 +1178,8 @@ acc_props(Ss) ->
|
||||
end, #{}, Ss).
|
||||
|
||||
s(S, #st{} = St) ->
|
||||
St#st{s = S, d = undefined}.
|
||||
Draft = schema_draft(S, St#st.draft, lists:reverse(St#st.p)),
|
||||
St#st{s = S, d = undefined, draft = Draft}.
|
||||
|
||||
push_path(Ps, #st{p = P0} = St) when is_list(Ps) ->
|
||||
%% Assume Ps is in reverse order
|
||||
@@ -977,7 +1241,7 @@ expand_schema(S) ->
|
||||
%% S#{<<"definitions">> := expand_schema(D, S)}.
|
||||
|
||||
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) ->
|
||||
%% https://json-schema.org/understanding-json-schema/structuring#dollarref
|
||||
%% When $id is used in a subschema, it indicates an embedded schema.
|
||||
@@ -1002,7 +1266,7 @@ expand_schema(S, _) ->
|
||||
S.
|
||||
|
||||
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};
|
||||
expand_schema_(K, V, Acc, S0) ->
|
||||
Acc#{K => expand_schema(V, S0)}.
|
||||
@@ -1010,13 +1274,13 @@ expand_schema_(K, V, Acc, S0) ->
|
||||
expand_ref(R, _, #{follow_refs := false}) ->
|
||||
R;
|
||||
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.
|
||||
%% This done by using `{"$ref" : "#"}`
|
||||
R;
|
||||
expand_ref(<<"#/", Path/binary>>, #st{r = S}) ->
|
||||
S;
|
||||
expand_ref(<<"#/", Path/binary>>, S) ->
|
||||
Key = filename:split(Path),
|
||||
case schema(Key, S, #{follow_refs => false}) of
|
||||
{ok, #{<<"$ref">> := _}} ->
|
||||
@@ -1036,63 +1300,8 @@ expand_ref(<<"#/", Path/binary>>, #st{r = S}) ->
|
||||
Def;
|
||||
undefined ->
|
||||
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.
|
||||
|
||||
%% 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, get_schema()).
|
||||
|
||||
@@ -1102,7 +1311,8 @@ schema(Path, Schema) ->
|
||||
schema(Path, #st{s = Schema, r = RootSchema}, Opts) ->
|
||||
schema_(Path, Schema, RootSchema, Opts);
|
||||
schema(Path, Schema, Opts) ->
|
||||
schema_(Path, Schema, Schema, Opts).
|
||||
St = new_state(Schema, Schema),
|
||||
schema(Path, St, Opts).
|
||||
|
||||
schema_([H|T], Schema, RootSchema0, Opts) ->
|
||||
RootSchema = set_rootschema(Schema, RootSchema0),
|
||||
|
||||
@@ -41,9 +41,148 @@ schema_spec_examples_test_() ->
|
||||
?t(t_ref_loop())
|
||||
, ?t(t_recursive_def())
|
||||
, ?t(t_nested_refs())
|
||||
, ?t(t_anchors())
|
||||
]}.
|
||||
|
||||
draft_discrimination_test_() ->
|
||||
{"JSON Schema draft discrimination",
|
||||
[
|
||||
?t(t_draft4_exclusive_bounds())
|
||||
, ?t(t_draft4_rejects_boolean_schema())
|
||||
, ?t(t_draft4_rejects_const())
|
||||
, ?t(t_draft6_accepts_const())
|
||||
, ?t(t_draft6_rejects_conditionals())
|
||||
, ?t(t_draft7_accepts_conditionals())
|
||||
, ?t(t_draft7_rejects_prefix_items())
|
||||
, ?t(t_draft2020_accepts_prefix_items())
|
||||
, ?t(t_draft2020_rejects_tuple_items())
|
||||
, ?t(t_unspecified_draft_remains_compatible())
|
||||
, ?t(t_unknown_draft_is_rejected())
|
||||
]}.
|
||||
|
||||
-define(DRAFT4, <<"http://json-schema.org/draft-04/schema#">>).
|
||||
-define(DRAFT6, <<"http://json-schema.org/draft-06/schema#">>).
|
||||
-define(DRAFT7, <<"http://json-schema.org/draft-07/schema#">>).
|
||||
-define(DRAFT2020, <<"https://json-schema.org/draft/2020-12/schema">>).
|
||||
|
||||
with_draft(Draft, Schema) ->
|
||||
Schema#{<<"$schema">> => Draft}.
|
||||
|
||||
t_draft4_exclusive_bounds() ->
|
||||
S = with_draft(?DRAFT4,
|
||||
#{<<"type">> => <<"number">>,
|
||||
<<"minimum">> => 1,
|
||||
<<"exclusiveMinimum">> => true,
|
||||
<<"maximum">> => 3,
|
||||
<<"exclusiveMaximum">> => false}),
|
||||
all_valid([2, 3], S),
|
||||
all_fail([1, 4], S, not_in_range),
|
||||
schema_fails(
|
||||
with_draft(?DRAFT4,
|
||||
#{<<"type">> => <<"number">>,
|
||||
<<"exclusiveMinimum">> => 1}),
|
||||
#{e => keyword_value_not_in_draft,
|
||||
draft => draft4,
|
||||
p => [<<"exclusiveMinimum">>]}),
|
||||
schema_fails(
|
||||
with_draft(?DRAFT4,
|
||||
#{<<"type">> => <<"number">>,
|
||||
<<"exclusiveMinimum">> => true}),
|
||||
#{e => missing_keyword_for_draft,
|
||||
draft => draft4,
|
||||
keyword => <<"exclusiveMinimum">>,
|
||||
required_keyword => <<"minimum">>}).
|
||||
|
||||
t_draft4_rejects_boolean_schema() ->
|
||||
S = with_draft(?DRAFT4,
|
||||
#{<<"properties">> => #{<<"disabled">> => false}}),
|
||||
schema_fails(S, #{e => boolean_schema_not_in_draft,
|
||||
draft => draft4,
|
||||
p => [<<"properties">>, <<"disabled">>]}),
|
||||
ObjectSchema = with_draft(?DRAFT4,
|
||||
#{<<"type">> => <<"object">>,
|
||||
<<"additionalProperties">> => false}),
|
||||
is_valid(#{}, ObjectSchema),
|
||||
fails(#{<<"unexpected">> => true}, ObjectSchema, invalid).
|
||||
|
||||
t_draft4_rejects_const() ->
|
||||
S = with_draft(?DRAFT4,
|
||||
#{<<"properties">> =>
|
||||
#{<<"mode">> => #{<<"const">> => <<"on">>}}}),
|
||||
schema_fails(S, #{e => keyword_not_in_draft,
|
||||
draft => draft4,
|
||||
keyword => <<"const">>,
|
||||
introduced_in => draft6,
|
||||
p => [<<"properties">>, <<"mode">>, <<"const">>]}).
|
||||
|
||||
t_draft6_accepts_const() ->
|
||||
S = with_draft(?DRAFT6, #{<<"const">> => <<"on">>}),
|
||||
is_valid(<<"on">>, S),
|
||||
fails(<<"off">>, S, not_in_enum).
|
||||
|
||||
t_draft6_rejects_conditionals() ->
|
||||
S = with_draft(?DRAFT6,
|
||||
#{<<"if">> => #{<<"const">> => 1},
|
||||
<<"then">> => false}),
|
||||
schema_fails(S, #{e => keyword_not_in_draft,
|
||||
draft => draft6,
|
||||
keyword => <<"if">>,
|
||||
introduced_in => draft7,
|
||||
p => [<<"if">>]}).
|
||||
|
||||
t_draft7_accepts_conditionals() ->
|
||||
S = with_draft(?DRAFT7,
|
||||
#{<<"if">> => #{<<"const">> => 1},
|
||||
<<"then">> => #{<<"minimum">> => 1},
|
||||
<<"else">> => #{<<"minimum">> => 2}}),
|
||||
all_valid([1, 2], S),
|
||||
fails(0, S, not_in_range).
|
||||
|
||||
t_draft7_rejects_prefix_items() ->
|
||||
S = with_draft(?DRAFT7,
|
||||
#{<<"type">> => <<"array">>,
|
||||
<<"prefixItems">> => [#{<<"type">> => <<"integer">>}]}),
|
||||
schema_fails(S, #{e => keyword_not_in_draft,
|
||||
draft => draft7,
|
||||
keyword => <<"prefixItems">>,
|
||||
introduced_in => draft2020_12,
|
||||
p => [<<"prefixItems">>]}).
|
||||
|
||||
t_draft2020_accepts_prefix_items() ->
|
||||
S = with_draft(?DRAFT2020,
|
||||
#{<<"type">> => <<"array">>,
|
||||
<<"prefixItems">> => [#{<<"type">> => <<"integer">>}],
|
||||
<<"items">> => false}),
|
||||
is_valid([1], S),
|
||||
fails([1, 2], S, invalid).
|
||||
|
||||
t_draft2020_rejects_tuple_items() ->
|
||||
S = with_draft(?DRAFT2020,
|
||||
#{<<"type">> => <<"array">>,
|
||||
<<"items">> => [#{<<"type">> => <<"integer">>}]}),
|
||||
schema_fails(S, #{e => keyword_value_not_in_draft,
|
||||
draft => draft2020_12,
|
||||
keyword => <<"items">>,
|
||||
p => [<<"items">>]}),
|
||||
Additional = with_draft(?DRAFT2020,
|
||||
#{<<"type">> => <<"array">>,
|
||||
<<"additionalItems">> => false}),
|
||||
schema_fails(Additional, #{e => keyword_not_in_draft,
|
||||
draft => draft2020_12,
|
||||
keyword => <<"additionalItems">>,
|
||||
last_supported_in => draft2019_09,
|
||||
p => [<<"additionalItems">>]}).
|
||||
|
||||
t_unspecified_draft_remains_compatible() ->
|
||||
S = #{<<"type">> => <<"array">>,
|
||||
<<"prefixItems">> => [#{<<"type">> => <<"integer">>}]},
|
||||
is_valid([1], S).
|
||||
|
||||
t_unknown_draft_is_rejected() ->
|
||||
S = with_draft(<<"https://example.test/schema-dialect">>, #{}),
|
||||
schema_fails(S, #{e => unsupported_schema_draft,
|
||||
draft_uri => <<"https://example.test/schema-dialect">>,
|
||||
p => [<<"$schema">>]}).
|
||||
|
||||
array() -> #{<<"type">> => <<"array">>}.
|
||||
int() -> #{<<"type">> => <<"integer">>}.
|
||||
str() -> #{<<"type">> => <<"string">>}.
|
||||
@@ -294,8 +433,7 @@ fails(V, S, Opts, Reason) when is_atom(Reason) ->
|
||||
fails(V, S, Opts, #{e => Reason});
|
||||
fails(V, S, Opts, Expect) ->
|
||||
try validate(V, S, Opts) of
|
||||
Other ->
|
||||
?debugFmt("Expected failure, Other = ~p", [Other]),
|
||||
_ ->
|
||||
error({expected_exception, #{v => V,
|
||||
s => S,
|
||||
e => Expect}})
|
||||
@@ -305,8 +443,16 @@ fails(V, S, Opts, Expect) ->
|
||||
end.
|
||||
%% ?assertError({Reason, [], V}, valid(V, S)).
|
||||
|
||||
match_expected('_', _) ->
|
||||
ok;
|
||||
schema_fails(Schema, Expect) ->
|
||||
try valid(null, Schema) of
|
||||
_ ->
|
||||
error({expected_schema_exception,
|
||||
#{s => Schema, e => Expect}})
|
||||
catch
|
||||
error:Reason ->
|
||||
match_expected(Expect, Reason)
|
||||
end.
|
||||
|
||||
match_expected(E, R) ->
|
||||
case maps:fold(
|
||||
fun(K, V, Acc) ->
|
||||
@@ -354,6 +500,7 @@ all_fail(Vs, S, Reason) ->
|
||||
read(F) ->
|
||||
FullF = filename:join(
|
||||
filename:dirname(code:which(?MODULE)), F),
|
||||
?debugFmt("FullF = ~s~n", [FullF]),
|
||||
{ok, Bin} = file:read_file(FullF),
|
||||
dec(Bin).
|
||||
|
||||
@@ -405,11 +552,3 @@ t_nested_refs() ->
|
||||
validate(Vs, S, Opts),
|
||||
fails(Vf, S, Opts, #{e => failing_schemas}),
|
||||
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