405 lines
14 KiB
Erlang
405 lines
14 KiB
Erlang
-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.
|