Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync from feat/customizable_key #294

Merged
merged 9 commits into from
May 23, 2024
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) ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this used anywhere? if not, i'd like to have this clause deleted.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is used in serverless

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
throw:Reason ->
{error, #{name => Name, cause => 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
91 changes: 67 additions & 24 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 @@ -2467,7 +2506,7 @@ map_atom_keys_test_() ->
{_, [
#{
kind := validation_error,
lafirest marked this conversation as resolved.
Show resolved Hide resolved
got := [BadKeyStr],
got := BadKeyStr,
reason := invalid_map_key
}
]},
Expand All @@ -2486,8 +2525,7 @@ map_atom_keys_test_() ->
?assertThrow(
{_, [
#{
kind := validation_error,
got := [BadKeyStr],
got := BadKeyStr,
reason := invalid_map_key
}
]},
Expand All @@ -2503,8 +2541,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