Add JSON Schema draft discrimination #7
@@ -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"`
|
||||
|
||||
+318
-10
@@ -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,10 +97,10 @@ 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()).
|
||||
@@ -107,6 +115,293 @@ normalize(S) ->
|
||||
bin_key(A) when is_atom(A) -> atom_to_binary(A, utf8);
|
||||
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'}),
|
||||
@@ -116,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.
|
||||
|
||||
@@ -129,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
|
||||
@@ -396,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);
|
||||
@@ -571,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))),
|
||||
@@ -578,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);
|
||||
@@ -872,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
|
||||
@@ -1004,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),
|
||||
|
||||
@@ -43,6 +43,146 @@ schema_spec_examples_test_() ->
|
||||
, ?t(t_nested_refs())
|
||||
]}.
|
||||
|
||||
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">>}.
|
||||
@@ -303,6 +443,16 @@ fails(V, S, Opts, Expect) ->
|
||||
end.
|
||||
%% ?assertError({Reason, [], V}, valid(V, S)).
|
||||
|
||||
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) ->
|
||||
|
||||
Reference in New Issue
Block a user