Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/feat/customizable_key' into sync-ck
Browse files Browse the repository at this point in the history
  • Loading branch information
lafirest committed May 21, 2024
2 parents a796199 + 451e1e7 commit 1a4981d
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 58 deletions.
22 changes: 18 additions & 4 deletions src/hocon_schema.erl
Original file line number Diff line number Diff line change
Expand Up @@ -348,11 +348,25 @@ find_structs_per_type(Schema, ?UNION(Types0, _), Acc, Stack, TStack, Opts) ->
Acc,
Types
);
find_structs_per_type(Schema, ?MAP(Name, Type), Acc, Stack, TStack, Opts) ->
find_structs_per_type(Schema, Type, Acc, ["$" ++ str(Name) | Stack], TStack, Opts);
find_structs_per_type(Schema, ?MAP(NameType, Type), Acc, Stack, TStack, Opts) ->
find_structs_per_type(
Schema, Type, Acc, ["$" ++ str(get_map_key_type_name(NameType)) | Stack], TStack, Opts
);
find_structs_per_type(_Schema, _Type, Acc, _Stack, _TStack, _Opts) ->
Acc.

get_map_key_type_name(Fun) when is_function(Fun, 1) ->
case Fun(name) of
undefined ->
name;
Name ->
Name
end;
get_map_key_type_name(Map) when is_map(Map) ->
maps:get(name, Map, name);
get_map_key_type_name(Name) ->
Name.

find_ref(Schema, Name, Acc, Stack, TStack, Opts) ->
Namespace = namespace(Schema),
Key = {Namespace, Schema, Name},
Expand Down Expand Up @@ -488,10 +502,10 @@ fmt_type(_Ns, ?ENUM(Symbols)) ->
};
fmt_type(Ns, ?LAZY(T)) ->
fmt_type(Ns, T);
fmt_type(Ns, ?MAP(Name, T)) ->
fmt_type(Ns, ?MAP(NameType, T)) ->
#{
kind => map,
name => bin(Name),
name => bin(get_map_key_type_name(NameType)),
values => fmt_type(Ns, T)
};
fmt_type(_Ns, Type) when ?IS_TYPEREFL(Type) ->
Expand Down
82 changes: 53 additions & 29 deletions src/hocon_tconf.erl
Original file line number Diff line number Diff line change
Expand Up @@ -551,15 +551,15 @@ map_field_maybe_convert(Type, Schema, Value0, Opts, Converter) ->
}
end.

map_field(?MAP(_Name, Type), FieldSchema, Value, Opts) ->
map_field(?MAP(NameType, Type), FieldSchema, Value, Opts) ->
%% map type always has string keys
Keys = maps_keys(unbox(Opts, Value)),
case [str(K) || K <- Keys] of
[] ->
{[], Value};
FieldNames ->
case get_invalid_name(FieldNames, Opts) of
[] ->
case check_map_key_name(NameType, FieldNames) of
ok ->
%% All objects in this map should share the same schema.
NewSc = hocon_schema:override(
FieldSchema,
Expand All @@ -568,13 +568,10 @@ map_field(?MAP(_Name, Type), FieldSchema, Value, Opts) ->
NewFields = [{FieldName, NewSc} || FieldName <- FieldNames],
%% start over
do_map(NewFields, Value, Opts, NewSc);
InvalidNames ->
{error, Meta} ->
Context =
#{
reason => invalid_map_key,
expected_data_type => ?MAP_KEY_RE,
hint => "map keys must have less than 255 bytes",
got => InvalidNames
Meta#{
reason => invalid_map_key
},
{validation_errs(Opts, Context), Value}
end
Expand Down Expand Up @@ -1307,26 +1304,53 @@ check_index_seq(I, [{Index, V} | Rest], Acc) ->
}}
end.

get_invalid_name(Names, Opts) ->
AtomKey =
case maps:get(atom_key, Opts, false) of
true -> true;
{true, unsafe} -> true;
_ -> false
end,
lists:filter(
fun(F) ->
DoesNotMatchRE =
try
nomatch =:= re:run(F, ?MAP_KEY_RE)
catch
_:_ -> false
end,
TooLong = AtomKey andalso string:length(F) > 255,
DoesNotMatchRE orelse TooLong
end,
Names
).
check_map_key_name(NameType, Names) ->
Validator = get_map_key_name_validator(NameType),
check_map_key_name2(Validator, Names).

check_map_key_name2(Validator, [Name | T]) ->
try Validator(Name) of
ok ->
check_map_key_name2(Validator, T);
{error, _Meta} = Error ->
Error
catch
_:Reason ->
{error, #{name => Name, message => Reason}}
end;
check_map_key_name2(_Validator, []) ->
ok.

get_map_key_name_validator(Fun) when is_function(Fun, 1) ->
case Fun(validator) of
undefined ->
fun default_map_key_name_validator/1;
Validator ->
Validator
end;
get_map_key_name_validator(Meta) when is_map(Meta) ->
maps:get(validator, Meta, fun default_map_key_name_validator/1);
get_map_key_name_validator(_NameType) ->
fun default_map_key_name_validator/1.

default_map_key_name_validator(Name) ->
case re:run(Name, ?MAP_KEY_RE) of
nomatch ->
{error, #{
expected_data_type => ?MAP_KEY_RE,
got => Name
}};
_ ->
case string:length(Name) > 255 of
true ->
{error, #{
hint => "map keys must have less than 255 bytes",
got => Name
}};
_ ->
ok
end
end.

fmt_field_names(Names) ->
do_fmt_field_names(lists:sort(lists:map(fun str/1, Names))).
Expand Down
2 changes: 2 additions & 0 deletions test/hocon_schema_example_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ fields("example") ->
)},
{map_key1, ?HOCON(hoconsc:map(map_name1, ?REF("map_name")), #{example1s => MapExample})},
{map_key2, ?HOCON(hoconsc:map(map_name2, ?REF("map_name")), #{})},
{map_key3, ?HOCON(hoconsc:map(fun(name) -> map_name3 end, ?REF("map_name")), #{})},
{map_key4, ?HOCON(hoconsc:map(#{name => map_key4}, ?REF("map_name")), #{})},
{union_key, ?HOCON(Union, #{examples => UnionExample})},
{union_key2, Union},
{array_key, hoconsc:array(?R_REF(emqx_schema, "mcast"))},
Expand Down
92 changes: 67 additions & 25 deletions test/hocon_tconf_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -389,27 +389,66 @@ mapping_test_() ->
)
].

map_key_test() ->
Sc = #{roots => [{"val", hoconsc:map(key, string())}]},
GoodConf = "val = {good_GOOD = value}",
{ok, GoodMap} = hocon:binary(GoodConf, #{format => map}),
?assertEqual(
#{<<"val">> => #{<<"good_GOOD">> => "value"}},
hocon_tconf:check_plain(Sc, GoodMap, #{apply_override_envs => false})
),
map_key_test_() ->
Checker = fun(KeyType, GoodConf, GoodResult, BadConfs) ->
Sc = #{roots => [{"val", hoconsc:map(KeyType, string())}]},
{ok, GoodMap} = hocon:binary(GoodConf, #{format => map}),
?assertEqual(
GoodResult,
hocon_tconf:check_plain(Sc, GoodMap, #{apply_override_envs => false})
),

BadConfs = ["val = {\"_bad\" = value}", "val = {\"bad_-n\" = value}"],
lists:foreach(
fun(BadConf) ->
{ok, BadMap} = hocon:binary(BadConf, #{format => map}),
?GEN_VALIDATION_ERR(
#{path := "val", reason := invalid_map_key},
hocon_tconf:check_plain(Sc, BadMap, #{apply_override_envs => false})
lists:foreach(
fun(BadConf) ->
{ok, BadMap} = hocon:binary(BadConf, #{format => map}),
?GEN_VALIDATION_ERR(
#{path := "val", reason := invalid_map_key},
hocon_tconf:check_plain(Sc, BadMap, #{apply_override_envs => false})
)
end,
BadConfs
)
end,
Normal =
{"map_key_test", fun() ->
Checker(
key,
"val = {good_GOOD = value}",
#{<<"val">> => #{<<"good_GOOD">> => "value"}},
["val = {\"_bad\" = value}", "val = {\"bad_-n\" = value}"]
)
end,
BadConfs
),
ok.
end},
Validator = fun(Name) ->
case re:run(Name, "[a-z]") of
nomatch ->
{error, #{}};
_ ->
ok
end
end,
Fun =
{"fun_map_key_test", fun() ->
Checker(
fun(validator) ->
Validator
end,
"val = {good = value}",
#{<<"val">> => #{<<"good">> => "value"}},
["val = {Bad = value}", "val = {bad1 = value}"]
)
end},
Struct =
{"structal_map_key_test", fun() ->
Checker(
#{
validator => Validator
},
"val = {good = value}",
#{<<"val">> => #{<<"good">> => "value"}},
["val = {Bad = value}", "val = {bad1 = value}"]
)
end},
[Normal, Fun, Struct].

generate_compatibility_test() ->
Conf = [
Expand Down Expand Up @@ -2466,8 +2505,7 @@ map_atom_keys_test_() ->
?assertThrow(
{_, [
#{
kind := validation_error,
got := [BadKeyStr],
got := BadKeyStr,
reason := invalid_map_key
}
]},
Expand All @@ -2486,8 +2524,7 @@ map_atom_keys_test_() ->
?assertThrow(
{_, [
#{
kind := validation_error,
got := [BadKeyStr],
got := BadKeyStr,
reason := invalid_map_key
}
]},
Expand All @@ -2503,8 +2540,13 @@ map_atom_keys_test_() ->
BadKeyStr = lists:duplicate(256, $a),
BadKey = list_to_binary(BadKeyStr),
BadMap = #{<<"root">> => #{BadKey => #{<<"foo">> => #{}}}},
?assertMatch(
#{<<"root">> := #{BadKey := _}},
?assertThrow(
{_, [
#{
got := BadKeyStr,
reason := invalid_map_key
}
]},
hocon_tconf:check_plain(
Sc,
BadMap,
Expand Down

0 comments on commit 1a4981d

Please sign in to comment.