diff --git a/src/hocon_schema.erl b/src/hocon_schema.erl index b61ca13..953f0e9 100644 --- a/src/hocon_schema.erl +++ b/src/hocon_schema.erl @@ -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}, @@ -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) -> diff --git a/src/hocon_tconf.erl b/src/hocon_tconf.erl index 04a3a38..8803b33 100644 --- a/src/hocon_tconf.erl +++ b/src/hocon_tconf.erl @@ -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, @@ -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 @@ -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))). diff --git a/test/hocon_schema_example_tests.erl b/test/hocon_schema_example_tests.erl index 854cbc4..045406e 100644 --- a/test/hocon_schema_example_tests.erl +++ b/test/hocon_schema_example_tests.erl @@ -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"))}, diff --git a/test/hocon_tconf_tests.erl b/test/hocon_tconf_tests.erl index 31a95ac..6372074 100644 --- a/test/hocon_tconf_tests.erl +++ b/test/hocon_tconf_tests.erl @@ -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 = [ @@ -2466,8 +2505,7 @@ map_atom_keys_test_() -> ?assertThrow( {_, [ #{ - kind := validation_error, - got := [BadKeyStr], + got := BadKeyStr, reason := invalid_map_key } ]}, @@ -2486,8 +2524,7 @@ map_atom_keys_test_() -> ?assertThrow( {_, [ #{ - kind := validation_error, - got := [BadKeyStr], + got := BadKeyStr, reason := invalid_map_key } ]}, @@ -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,