Validator extensions, zomp vsn 0.2.0 (#5)
Co-authored-by: Ulf Wiger <ulf@wiger.net> Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
@@ -0,0 +1,404 @@
|
||||
-module(gmconfig_schema_utils_tests).
|
||||
|
||||
-export([tr/0]).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-import(gmconfig_schema_utils, [ valid/2
|
||||
, validate/3 ]).
|
||||
|
||||
%%-define(t(Expr), ?_test(?debugVal(Expr))).
|
||||
-define(t(Expr), ?_test(Expr)).
|
||||
|
||||
tr() ->
|
||||
dbg:tracer(),
|
||||
dbg:tpl(?MODULE, x),
|
||||
dbg:tpl(gmconfig_schema_utils,x),
|
||||
dbg:p(all,[c]),
|
||||
eunit:test(?MODULE).
|
||||
|
||||
simple_type_test_() ->
|
||||
{"Simple type tests",
|
||||
[
|
||||
?t(t_integer())
|
||||
, ?t(t_number())
|
||||
, ?t(t_boolean())
|
||||
, ?t(t_string())
|
||||
, ?t(t_array())
|
||||
, ?t(t_object())
|
||||
, ?t(t_shortcut_schema())
|
||||
]}.
|
||||
|
||||
update_test_() ->
|
||||
{"Validated update tests",
|
||||
[
|
||||
?t(t_update_objects())
|
||||
]}.
|
||||
|
||||
schema_spec_examples_test_() ->
|
||||
{"Examples from JSON-Schema docs",
|
||||
[
|
||||
?t(t_ref_loop())
|
||||
, ?t(t_recursive_def())
|
||||
, ?t(t_nested_refs())
|
||||
]}.
|
||||
|
||||
array() -> #{<<"type">> => <<"array">>}.
|
||||
int() -> #{<<"type">> => <<"integer">>}.
|
||||
str() -> #{<<"type">> => <<"string">>}.
|
||||
obj(Ps) -> #{<<"type">> => <<"object">>,
|
||||
<<"properties">> => Ps}.
|
||||
|
||||
|
||||
t_update_objects() ->
|
||||
S0 = obj(
|
||||
#{<<"a">> => int(),
|
||||
<<"b">> => obj(#{<<"b1">> => array(),
|
||||
<<"b2">> =>
|
||||
obj(#{<<"b21">> => str()})
|
||||
})
|
||||
}),
|
||||
%%
|
||||
%% Only objects are merged; other types are replaced.
|
||||
A0 = #{<<"a">> => 1,
|
||||
<<"b">> => #{<<"b1">> => [1,2],
|
||||
<<"b2">> => #{}}},
|
||||
B0 = #{<<"a">> => 2,
|
||||
<<"b">> => #{<<"b1">> => [4,5],
|
||||
<<"b2">> => #{<<"b21">> => <<"foo">>}}},
|
||||
#{<<"a">> := 1,
|
||||
<<"b">> := #{<<"b1">> := [1,2],
|
||||
<<"b2">> := #{<<"b21">> := <<"foo">>}}} =
|
||||
gmconfig_schema_utils:merge(A0, B0, S0),
|
||||
%%
|
||||
%% Modified update semantics - replace instead of merge
|
||||
S0Ps = maps:get(<<"properties">>, S0),
|
||||
S01Ps = S0Ps#{<<"c">> => #{<<"type">> => <<"object">>,
|
||||
<<"updateSemantics">> => <<"replace">>,
|
||||
<<"properties">> =>
|
||||
#{<<"c1">> => #{<<"type">> => <<"integer">>},
|
||||
<<"c2">> => #{<<"type">> => <<"integer">>}
|
||||
}}},
|
||||
S01 = S0#{<<"properties">> => S01Ps},
|
||||
A01 = A0#{<<"c">> => #{<<"c1">> => 1}},
|
||||
B01 = B0#{<<"c">> => #{<<"c2">> => 2}},
|
||||
#{<<"a">> := 1,
|
||||
<<"b">> := #{<<"b1">> := [1,2],
|
||||
<<"b2">> := #{<<"b21">> := <<"foo">>}},
|
||||
<<"c">> := #{<<"c1">> := 1}} =
|
||||
gmconfig_schema_utils:merge(A01, B01, S01),
|
||||
A0r = #{<<"a">> => 1,
|
||||
<<"b">> => #{<<"$updateSemantics">> => <<"replace">>,
|
||||
<<"b1">> => [1,2],
|
||||
<<"b2">> => #{<<"$updateSemantics">> => <<"merge">>}
|
||||
}},
|
||||
#{<<"a">> := 1,
|
||||
<<"b">> := #{<<"b1">> := [1,2],
|
||||
<<"b2">> := B2M}} =
|
||||
gmconfig_schema_utils:merge(A0r, B0, S0),
|
||||
%% B2M is the empty map (updateSemantics prop removed recursively)
|
||||
?assertEqual(#{}, B2M),
|
||||
A0r1 = #{<<"a">> => 1,
|
||||
<<"b">> => #{<<"$updateSemantics">> => <<"merge">>,
|
||||
<<"b1">> => [1,2],
|
||||
<<"b2">> => #{<<"$updateSemantics">> => <<"replace">>}
|
||||
}},
|
||||
#{<<"a">> := 1,
|
||||
<<"b">> := #{<<"b1">> := [1,2],
|
||||
<<"b2">> := B2M1}} =
|
||||
gmconfig_schema_utils:merge(A0r1, B0, S0),
|
||||
%% B2M is the empty map (updateSemantics prop removed recursively)
|
||||
?assertEqual(#{}, B2M1),
|
||||
%%
|
||||
%% With updateSemantics : suggest, the offered data is accepted if
|
||||
%% the existing is missing or 'null'.
|
||||
S02Ps = S0Ps#{<<"c">> => #{<<"type">> => <<"object">>,
|
||||
<<"updateSemantics">> => <<"suggest">>,
|
||||
<<"properties">> =>
|
||||
#{<<"c1">> => #{<<"type">> => <<"integer">>},
|
||||
<<"c2">> => #{<<"type">> => <<"integer">>}
|
||||
}}},
|
||||
S02 = S0#{<<"properties">> => S02Ps},
|
||||
A02 = A0#{<<"c">> => #{<<"c1">> => 1}},
|
||||
B02 = B0#{<<"c">> => #{<<"c2">> => 2}},
|
||||
#{<<"a">> := 1,
|
||||
<<"b">> := #{<<"b1">> := [1,2],
|
||||
<<"b2">> := #{<<"b21">> := <<"foo">>}},
|
||||
<<"c">> := #{<<"c2">> := 2}} =
|
||||
gmconfig_schema_utils:merge(A02, B02, S02),
|
||||
B03 = B0#{<<"c">> => null},
|
||||
#{<<"a">> := 1,
|
||||
<<"b">> := #{<<"b1">> := [1,2],
|
||||
<<"b2">> := #{<<"b21">> := <<"foo">>}},
|
||||
<<"c">> := #{<<"c1">> := 1}} =
|
||||
gmconfig_schema_utils:merge(A02, B0, S02),
|
||||
#{<<"a">> := 1,
|
||||
<<"b">> := #{<<"b1">> := [1,2],
|
||||
<<"b2">> := #{<<"b21">> := <<"foo">>}},
|
||||
<<"c">> := #{<<"c1">> := 1}} =
|
||||
gmconfig_schema_utils:merge(A02, B03, S02),
|
||||
ok.
|
||||
|
||||
t_integer() ->
|
||||
S0 = #{<<"type">> => <<"integer">>},
|
||||
is_valid(17, S0),
|
||||
all_fail([17.0, [], #{}, <<>>], S0, wrong_type),
|
||||
S1 = S0#{<<"minimum">> => 1, <<"maximum">> => 4},
|
||||
all_valid([1, 2, 4], S1),
|
||||
all_fail([0, 5], S1, not_in_range),
|
||||
S2 = S0#{<<"exclusiveMinimum">> => 1, <<"exclusiveMaximum">> => 3},
|
||||
is_valid(2, S2),
|
||||
all_fail([1, 3], S2, not_in_range),
|
||||
S3 = S0#{<<"multipleOf">> => 2},
|
||||
is_valid(4, S3),
|
||||
fails(3, S3, not_a_multiple),
|
||||
ok.
|
||||
|
||||
t_number() ->
|
||||
S0 = #{<<"type">> => <<"number">>},
|
||||
all_valid([17, 17.0], S0),
|
||||
all_fail([#{}, [], <<>>, true], S0, wrong_type),
|
||||
S1 = S0#{<<"minimum">> => 1, <<"maximum">> => 4.0},
|
||||
all_valid([1, 1.0, 2, 2.3, 3.999, 4, 4.0], S1),
|
||||
all_fail([0, 5], S1, not_in_range),
|
||||
S2 = S0#{<<"exclusiveMinimum">> => 1, <<"exclusiveMaximum">> => 3},
|
||||
is_valid(2, S2),
|
||||
all_fail([1, 3], S2, not_in_range),
|
||||
S3 = S0#{<<"multipleOf">> => 2},
|
||||
is_valid(4, S3),
|
||||
all_fail([3, 4.0], S3, not_a_multiple),
|
||||
ok.
|
||||
|
||||
t_boolean() ->
|
||||
S0 = #{<<"type">> => <<"boolean">>},
|
||||
all_valid([true, false], S0),
|
||||
%% truthy and falsy types not accepted
|
||||
all_fail([1, 0, null, [], <<>>, #{}], S0, wrong_type),
|
||||
ok.
|
||||
|
||||
t_string() ->
|
||||
S0 = #{<<"type">> => <<"string">>},
|
||||
all_valid([<<>>, <<"foo">>], S0),
|
||||
fails("foo", S0, wrong_type),
|
||||
ok.
|
||||
|
||||
t_array() ->
|
||||
S0 = #{<<"type">> => <<"array">>},
|
||||
all_valid([ [], [1,1], [1,2,3], [#{}] ], S0),
|
||||
all_fail([1, false, #{}, <<"foo">>], S0, wrong_type),
|
||||
S1 = S0#{<<"minItems">> => 2},
|
||||
all_valid([[1,2], [1,2,3,4]], S1),
|
||||
all_fail([[], [1]], S1, not_in_range),
|
||||
S2 = S0#{<<"maxItems">> => 4},
|
||||
all_valid([[], [1], [1,2], [1,2,3,4]], S2),
|
||||
fails([1,2,3,4,5], S2, not_in_range),
|
||||
S3 = maps:merge(S1, S2),
|
||||
all_valid([ [1,2], [1,2,3], [1,2,3,4] ], S3),
|
||||
all_fail([ [1], [1,2,3,4,5] ], S3, not_in_range),
|
||||
S4 = S0#{<<"uniqueItems">> => true},
|
||||
all_valid([ [], [1], [1,2] ], S4),
|
||||
all_fail([ [1,1], [2,1,2] ], S4, not_unique),
|
||||
S5 = S0#{<<"items">> => #{<<"type">> => <<"integer">>}},
|
||||
all_valid([ [], [1], [1,2,3] ], S5),
|
||||
fails([<<"foo">>], S5, #{e => wrong_type, p => [array,items,0,integer]}),
|
||||
fails([1,2,false], S5, #{e => wrong_type, p => [array,items,2,integer]}),
|
||||
S6 = S0#{<<"prefixItems">> => [#{<<"type">> => <<"string">>},
|
||||
#{<<"type">> => <<"integer">>},
|
||||
#{<<"type">> => <<"integer">>}]},
|
||||
is_valid([<<"foo">>, 1], S6),
|
||||
is_valid([<<"foo">>, 1, 2], S6),
|
||||
is_valid([<<"foo">>, 1, 2, 3], S6),
|
||||
fails([1,2,3], S6, #{e => wrong_type, p => [array,prefixItems,0,string]}),
|
||||
S7 = S6#{<<"items">> => false},
|
||||
fails([<<"foo">>, 1, 2, 3], S7, #{e => invalid,
|
||||
p => [array,prefixItems,items,3]}),
|
||||
S71 = S6#{<<"items">> => #{<<"type">> => <<"integer">>}},
|
||||
is_valid([<<>>, 1,2,3,4,5,6], S71),
|
||||
fails([<<>>, 1,2,3,4,5,[]], S71,
|
||||
#{e => wrong_type,
|
||||
p => [array,prefixItems,items,6,integer]}),
|
||||
S8 = S0#{<<"contains">> => #{<<"type">> => <<"integer">>}},
|
||||
is_valid([1,2,3], S8),
|
||||
is_valid([true,true,1], S8),
|
||||
fails([true], S8, contains),
|
||||
fails([], S8, contains), % not sure where specifed, but makes sense
|
||||
S81 = S8#{<<"minContains">> => 2},
|
||||
is_valid([1,2,3], S81),
|
||||
is_valid([true,1,2], S81),
|
||||
fails([1], S81, #{e => not_in_range, p => [array, contains, min,
|
||||
integer, minimum]}),
|
||||
S82 = S8#{<<"maxContains">> => 4},
|
||||
all_valid([ [1], [1,2,3], [true,1,2,3,4] ], S82),
|
||||
S83 = maps:merge(S81, S82),
|
||||
is_valid([1,2,3], S83),
|
||||
fails([1], S83, #{e => not_in_range, p => [array, contains, min,
|
||||
integer, minimum]}),
|
||||
fails([1,2,3,4,5], S83, #{e => not_in_range, p => [array, contains, max,
|
||||
integer, maximum]}),
|
||||
is_valid([], S8#{<<"minContains">> => 0}),
|
||||
ok.
|
||||
|
||||
t_object() ->
|
||||
S0 = #{<<"type">> => <<"object">>},
|
||||
all_valid([ #{}, #{<<"a">> => 1} ], S0),
|
||||
all_fail([ true, 17, 17.0, [], <<"foo">> ], S0, #{e => wrong_type}),
|
||||
S1 = S0#{<<"properties">> => #{<<"a">> => #{<<"type">> => <<"integer">>}}},
|
||||
all_valid([ #{<<"a">> => 1}, #{}, #{<<"b">> => 1} ], S1),
|
||||
fails(#{<<"a">> => true}, S1, #{e => wrong_type,
|
||||
p => [object, <<"a">>, integer]}),
|
||||
is_valid(#{<<"b">> => 1}, S1#{<<"additionalProperties">> => true}),
|
||||
fails(#{<<"b">> => 1}, S1#{<<"additionalProperties">> => false},
|
||||
#{e => invalid,
|
||||
p => [object, <<"b">>],
|
||||
a => [additionalProperties]}),
|
||||
fails(#{<<"b">> => 1}, S1#{<<"required">> => [<<"a">>]},
|
||||
#{e => required, p => [object,required], a => [[<<"a">>]]}),
|
||||
all_valid([ #{<<"a">> => 1}, #{<<"a">> => true} ],
|
||||
S0#{<<"properties">> =>
|
||||
#{<<"a">> => #{<<"oneOf">> =>
|
||||
[#{<<"type">> => <<"integer">>},
|
||||
#{<<"type">> => <<"boolean">>}]}
|
||||
}}),
|
||||
S2 = S0#{<<"properties">> =>
|
||||
#{<<"a">> => #{<<"type">> => <<"integer">>,
|
||||
<<"allOf">> =>
|
||||
[#{<<"minimum">> => 2},
|
||||
#{<<"maximum">> => 5},
|
||||
#{<<"multipleOf">> => 2}
|
||||
]}}
|
||||
},
|
||||
is_valid(#{<<"a">> => 4}, S2),
|
||||
fails(#{<<"a">> => 3}, S2, #{e => failing_schemas,
|
||||
a => [[{2,'_'}]|'_'],
|
||||
p => [object, <<"a">>, allOf]}),
|
||||
ok.
|
||||
|
||||
t_shortcut_schema() ->
|
||||
Vs = [<<"foo">>,
|
||||
#{<<"a">> => 1},
|
||||
17],
|
||||
all_valid(Vs, #{}),
|
||||
all_valid(Vs, true),
|
||||
all_fail(Vs, false, invalid),
|
||||
ok.
|
||||
|
||||
is_valid(V, S) ->
|
||||
{V, V} = {valid(V, S), V}.
|
||||
%% ?assertEqual(V, valid(V, S)).
|
||||
|
||||
fails(V, S, Reason) ->
|
||||
fails(V, S, #{}, Reason).
|
||||
|
||||
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
|
||||
_ ->
|
||||
error({expected_exception, #{v => V,
|
||||
s => S,
|
||||
e => Expect}})
|
||||
catch
|
||||
error:R ->
|
||||
match_expected(Expect, R)
|
||||
end.
|
||||
%% ?assertError({Reason, [], V}, valid(V, S)).
|
||||
|
||||
match_expected(E, R) ->
|
||||
case maps:fold(
|
||||
fun(K, V, Acc) ->
|
||||
case maps:find(K, R) of
|
||||
{ok, V} -> Acc;
|
||||
{ok, Other} ->
|
||||
case match_other(V, Other) of
|
||||
true -> Acc;
|
||||
false ->
|
||||
Acc#{K => Other}
|
||||
end;
|
||||
error ->
|
||||
Acc#{K => '$not_found'}
|
||||
end
|
||||
end, #{}, E) of
|
||||
M when map_size(M) == 0 ->
|
||||
ok;
|
||||
Unexpected ->
|
||||
error({expected, E, Unexpected})
|
||||
end.
|
||||
|
||||
match_other(V, V) -> true;
|
||||
match_other([H1|T1], [H2|T2]) ->
|
||||
case match_other(H1, H2) of
|
||||
true ->
|
||||
match_other(T1, T2);
|
||||
false ->
|
||||
false
|
||||
end;
|
||||
match_other(T1, T2) when tuple_size(T1) =:= tuple_size(T2) ->
|
||||
lists:all(fun({A,B}) -> match_other(A, B) end,
|
||||
lists:zip(tuple_to_list(T1),
|
||||
tuple_to_list(T2)));
|
||||
match_other('_', _) ->
|
||||
true;
|
||||
match_other(_, _) ->
|
||||
false.
|
||||
|
||||
all_valid(Vs, S) ->
|
||||
[is_valid(V, S) || V <- Vs].
|
||||
|
||||
all_fail(Vs, S, Reason) ->
|
||||
[fails(V, S, Reason) || V <- Vs].
|
||||
|
||||
read(F) ->
|
||||
FullF = filename:join(
|
||||
filename:dirname(code:which(?MODULE)), F),
|
||||
?debugFmt("FullF = ~s~n", [FullF]),
|
||||
{ok, Bin} = file:read_file(FullF),
|
||||
dec(Bin).
|
||||
|
||||
dec(Str) ->
|
||||
try json:decode(Str)
|
||||
catch
|
||||
error:E ->
|
||||
error(#{type => json_decode_error,
|
||||
error => E})
|
||||
end.
|
||||
|
||||
|
||||
t_ref_loop() ->
|
||||
S = read("data/ref_loop_schema.json"),
|
||||
try gmconfig_schema_utils:schema([<<"$defs">>,<<"alice">>], S) of
|
||||
Unexpected ->
|
||||
error({unexpected, Unexpected})
|
||||
catch
|
||||
error:nested_references ->
|
||||
ok
|
||||
end.
|
||||
|
||||
t_recursive_def() ->
|
||||
S = read("data/recursion_schema.json"),
|
||||
D = read("data/recursion_data.json"),
|
||||
Df = read("data/recursion_data_faulty.json"),
|
||||
is_valid(D, S),
|
||||
fails(Df, S, #{e => wrong_type, p => [object, <<"children">>,
|
||||
array, items, 0,
|
||||
object, <<"children">>,
|
||||
array, items, 0,
|
||||
object,<<"children">>,
|
||||
array, items, 0,
|
||||
object, <<"name">>, string
|
||||
]}),
|
||||
ok.
|
||||
|
||||
t_nested_refs() ->
|
||||
S = read("data/nested_refs_schema.json"),
|
||||
F = fun(Str, #{<<"tags">> := Tags}) ->
|
||||
true = lists:any(
|
||||
fun(T) ->
|
||||
nomatch =/= string:prefix(Str, T)
|
||||
end, Tags)
|
||||
end,
|
||||
Opts = #{extensions => #{<<"x-serialization">> => F}},
|
||||
Vs = #{<<"tx">> => #{<<"from">> => <<"ak_good">>}},
|
||||
Vf = #{<<"tx">> => #{<<"from">> => <<"ac_bad">>}},
|
||||
validate(Vs, S, Opts),
|
||||
fails(Vf, S, Opts, #{e => failing_schemas}),
|
||||
ok.
|
||||
Reference in New Issue
Block a user