%% ------------------------------------------------------------------- %% %% cuttlefish_mapping: models a single mapping %% %% Copyright (c) 2013 Basho Technologies, Inc. All Rights Reserved. %% %% This file is provided to you under the Apache License, %% Version 2.0 (the "License"); you may not use this file %% except in compliance with the License. You may obtain %% a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, %% software distributed under the License is distributed on an %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY %% KIND, either express or implied. See the License for the %% specific language governing permissions and limitations %% under the License. %% %% ------------------------------------------------------------------- -module(cuttlefish_mapping). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -endif. -type collection_type() :: list | {map, atom} | {map, binary} | {proplist, atom}. -record(mapping, { variable::cuttlefish_variable:variable(), mapping::string(), default::term(), commented::term(), datatype = [string] :: cuttlefish_datatypes:datatype_list(), level = basic :: basic | intermediate | advanced, doc = [] :: list(), include_default = undefined :: string() | undefined, new_conf_value = undefined :: string() | undefined, validators = [] :: [string()], is_merge = false :: boolean(), see = [] :: [cuttlefish_variable:variable()], hidden = false :: boolean(), aliases = [] :: [{cuttlefish_variable:variable(), string() | undefined}], collect = undefined :: collection_type() | undefined }). -type mapping() :: #mapping{}. -type raw_mapping() :: {mapping, string(), string(), [proplists:property()]}. -export_type([mapping/0, collection_type/0]). -export([ parse/1, parse_and_merge/2, is_mapping/1, variable/1, is_fuzzy_variable/1, mapping/1, default/1, has_default/1, commented/1, datatype/1, level/1, hidden/1, doc/1, see/1, include_default/1, new_conf_value/1, replace/2, validators/1, validators/2, remove_all_but_first/2, aliases/1, aliases_with_messages/1, collect/1 ]). -spec parse(raw_mapping()) -> mapping() | cuttlefish_error:error(). parse({mapping, Variable, Mapping, Proplist}) -> Datatype = case proplists:get_value(datatype, Proplist, string) of {enum, Enums} -> AtomEnums = [ begin case is_list(E) of true -> list_to_atom(E); _ -> E end end || E <- Enums ], [{enum, AtomEnums}]; L when is_list(L) -> case cuttlefish_datatypes:is_valid_list(L) of true -> L; _ -> {error, {mapping_types, L}} end; D -> [D] end, VarTokenized = cuttlefish_variable:tokenize(Variable), IsMerge = proplists:is_defined(merge, Proplist), AliasesResult = parse_aliases(Variable, VarTokenized, Proplist, IsMerge), CollectResult = parse_collect(proplists:get_value(collect, Proplist)), case {Datatype, AliasesResult, CollectResult} of {{error, _}, _, _} -> Datatype; {_, {error, _} = AliasError, _} -> AliasError; {_, _, {error, _} = CollectError} -> CollectError; {_, TokenizedAliases, CollectType} -> WildcardCount = length([S || S <- VarTokenized, hd(S) =:= $$]), CollectError = case {CollectType, WildcardCount} of {undefined, _} -> ok; {_, 0} -> {error, {collect_on_non_fuzzy, Variable}}; {_, N} when N > 1 -> {error, {collect_multi_wildcard, Variable, N}}; {_, 1} -> ok end, case CollectError of {error, _} -> CollectError; ok -> #mapping{ variable = VarTokenized, default = proplists:get_value(default, Proplist), commented = proplists:get_value(commented, Proplist), mapping = Mapping, level = proplists:get_value(level, Proplist, basic), datatype = Datatype, doc = proplists:get_value(doc, Proplist, []), see = proplists:get_value(see, Proplist, []), include_default = proplists:get_value(include_default, Proplist), new_conf_value = proplists:get_value(new_conf_value, Proplist), validators = proplists:get_value(validators, Proplist, []), hidden = proplists:get_value(hidden, Proplist, false), aliases = TokenizedAliases, collect = CollectType } end end; parse(X) -> {error, {mapping_parse, X}}. -spec parse_aliases(string(), cuttlefish_variable:variable(), [proplists:property()], boolean()) -> [{cuttlefish_variable:variable(), string() | undefined}] | cuttlefish_error:error(). parse_aliases(Variable, VarTokenized, Proplist, IsMerge) -> RawAliases = case {proplists:get_value(alias, Proplist), proplists:get_value(aliases, Proplist)} of {undefined, undefined} -> undefined; {Single, undefined} when is_list(Single) -> [Single]; {{S, M} = Single, undefined} when is_list(S), is_list(M) -> [Single]; {Single, undefined} -> {error, {alias_not_a_string, Variable, Single}}; {undefined, Multi} -> Multi; {_, _} -> {error, {alias_and_aliases_both_set, Variable}} end, validate_raw_aliases(Variable, VarTokenized, RawAliases, IsMerge). validate_raw_aliases(_Variable, _VarTokenized, undefined, _IsMerge) -> []; validate_raw_aliases(_Variable, _VarTokenized, [] = _RawAliases, true = _IsMerge) -> []; validate_raw_aliases(Variable, _VarTokenized, [], _IsMerge) -> {error, {aliases_empty, Variable}}; validate_raw_aliases(_Variable, _VarTokenized, {error, _} = Error, _IsMerge) -> Error; validate_raw_aliases(Variable, _VarTokenized, [H|_], _IsMerge) when is_integer(H) -> %% Bare string: {aliases, "old.key"} instead of {aliases, ["old.key"]} {error, {aliases_is_bare_string, Variable}}; validate_raw_aliases(Variable, VarTokenized, RawAliases, _IsMerge) when is_list(RawAliases) -> case normalize_alias_entries(Variable, RawAliases) of {error, _} = E -> E; Entries -> TokenizedEntries = [{cuttlefish_variable:tokenize(A), Msg} || {A, Msg} <- Entries], TokenizedVars = [Var || {Var, _} <- TokenizedEntries], case lists:member(VarTokenized, TokenizedVars) of true -> {error, {alias_is_self, Variable}}; false -> case length(lists:usort(TokenizedVars)) < length(TokenizedVars) of true -> {error, {aliases_contain_duplicates, Variable}}; false -> case first_fuzzy_alias(TokenizedVars) of {fuzzy, FuzzyAlias} -> {error, {fuzzy_alias_unsupported, Variable, cuttlefish_variable:format(FuzzyAlias)}}; ok -> TokenizedEntries end end end end; validate_raw_aliases(Variable, _VarTokenized, BadValue, _IsMerge) -> {error, {aliases_invalid_value, Variable, BadValue}}. normalize_alias_entries(_Variable, []) -> []; normalize_alias_entries(Variable, [H|T]) -> case normalize_alias_entry(Variable, H) of {error, _} = E -> E; Entry -> case normalize_alias_entries(Variable, T) of {error, _} = E2 -> E2; Rest -> [Entry | Rest] end end. normalize_alias_entry(_Variable, AliasStr) when is_list(AliasStr) -> {AliasStr, undefined}; normalize_alias_entry(_Variable, {AliasStr, Message}) when is_list(AliasStr), is_list(Message) -> {AliasStr, Message}; normalize_alias_entry(Variable, Bad) -> {error, {alias_not_a_string, Variable, Bad}}. -spec first_fuzzy_alias([cuttlefish_variable:variable()]) -> ok | {fuzzy, cuttlefish_variable:variable()}. first_fuzzy_alias([]) -> ok; first_fuzzy_alias([Alias|Rest]) -> IsFuzzy = lists:any(fun([$$|_]) -> true; (_) -> false end, Alias), case IsFuzzy of true -> {fuzzy, Alias}; false -> first_fuzzy_alias(Rest) end. %% If this mapping exists, do something. %% That something is usually a simple replace, unless the proplist in %% the raw mapping contains the atom 'merge'. %% %% This function assumes it's run as part of a foldl over new schema elements %% in which case, there's only ever one instance of a key in the list %% so keyreplace works fine. -spec parse_and_merge( raw_mapping(), [mapping()]) -> [mapping()] | cuttlefish_error:error(). parse_and_merge({mapping, Variable, _Mapping, Props} = MappingSource, Mappings) -> Var = cuttlefish_variable:tokenize(Variable), case lists:keyfind(Var, #mapping.variable, Mappings) of false -> [ parse(MappingSource) | Mappings]; OldMapping -> MaybeMergedMapping = case proplists:is_defined(merge, Props) of true -> merge(MappingSource, OldMapping); _ -> parse(MappingSource) end, case MaybeMergedMapping of {error, _} = Error -> Error; _ -> lists:keyreplace(Var, #mapping.variable, Mappings, MaybeMergedMapping) end end. -spec merge(raw_mapping(), mapping()) -> mapping() | cuttlefish_error:error(). merge(NewMappingSource, OldMapping) -> case parse(NewMappingSource) of {error, _} = Error -> Error; MergeMapping -> #mapping{ variable = variable(MergeMapping), mapping = mapping(MergeMapping), default = choose(default, NewMappingSource, MergeMapping, OldMapping), commented = choose(commented, NewMappingSource, MergeMapping, OldMapping), datatype = choose(datatype, NewMappingSource, MergeMapping, OldMapping), level = choose(level, NewMappingSource, MergeMapping, OldMapping), doc = choose(doc, NewMappingSource, MergeMapping, OldMapping), include_default = choose(include_default, NewMappingSource, MergeMapping, OldMapping), new_conf_value = choose(new_conf_value, NewMappingSource, MergeMapping, OldMapping), validators = choose(validators, NewMappingSource, MergeMapping, OldMapping), see = choose(see, NewMappingSource, MergeMapping, OldMapping), hidden = choose(hidden, NewMappingSource, MergeMapping, OldMapping), aliases = choose(aliases, NewMappingSource, MergeMapping, OldMapping), collect = choose(collect, NewMappingSource, MergeMapping, OldMapping) } end. choose(Field, {_, _, _, PreParseMergeProps}, MergeMapping, OldMapping) -> IsDefined = proplists:is_defined(Field, PreParseMergeProps) orelse (Field =:= aliases andalso proplists:is_defined(alias, PreParseMergeProps)), Value = proplists:get_value(Field, PreParseMergeProps), %% Empty see/doc in a merge mapping is treated as "not specified" (inherit), %% but empty aliases is an intentional "clear all aliases" directive. Which = case {Field, IsDefined, Value} of {see, _, []} -> old; {doc, _, []} -> old; {_, true, _} -> new; _ -> old end, case {Which, Field} of {new, aliases} -> aliases_with_messages(MergeMapping); {old, aliases} -> aliases_with_messages(OldMapping); {new, _} -> ?MODULE:Field(MergeMapping); {old, _} -> ?MODULE:Field(OldMapping) end. -spec is_mapping(any()) -> boolean(). is_mapping(M) -> is_tuple(M) andalso element(1, M) =:= mapping. -spec variable(mapping()) -> [string()]. variable(M) -> M#mapping.variable. -spec is_fuzzy_variable(mapping()) -> boolean(). is_fuzzy_variable(#mapping{variable=VariableDef}) -> lists:any(fun(X) -> hd(X) =:= $$ end, VariableDef). -spec mapping(mapping()) -> string(). mapping(M) -> M#mapping.mapping. -spec default(mapping()) -> term(). default(M) -> M#mapping.default. -spec has_default(mapping()) -> boolean(). has_default(MappingRecord) -> default(MappingRecord) =/= undefined. -spec commented(mapping()) -> term(). commented(M) -> M#mapping.commented. -spec datatype(mapping()) -> cuttlefish_datatypes:datatype_list(). datatype(M) -> M#mapping.datatype. -spec level(mapping()) -> basic | intermediate | advanced. level(M) -> M#mapping.level. -spec hidden(mapping()) -> boolean(). hidden(M) -> M#mapping.hidden. -spec doc(mapping()) -> [string()]. doc(M) -> M#mapping.doc. -spec see(mapping()) -> [cuttlefish_variable:variable()]. see(M) -> M#mapping.see. -spec include_default(mapping()) -> string() | undefined. include_default(M) -> M#mapping.include_default. -spec new_conf_value(mapping()) -> string() | undefined. new_conf_value(M) -> M#mapping.new_conf_value. -spec validators(mapping()) -> [string()]. validators(M) -> M#mapping.validators. -spec aliases(mapping()) -> [cuttlefish_variable:variable()]. aliases(M) -> [Var || {Var, _Msg} <- M#mapping.aliases]. -spec aliases_with_messages(mapping()) -> [{cuttlefish_variable:variable(), string() | undefined}]. aliases_with_messages(M) -> M#mapping.aliases. -spec collect(mapping()) -> collection_type() | undefined. collect(M) -> M#mapping.collect. -spec validators(mapping(), [cuttlefish_validator:validator()]) -> [cuttlefish_validator:validator()]. validators(M, Validators) -> lists:foldr(fun(VName, Vs) -> case lists:keyfind(VName, 2, Validators) of false -> Vs; V -> [V|Vs] end end, [], M#mapping.validators). -spec replace(mapping(), [mapping()]) -> [mapping()]. replace(Mapping, ListOfMappings) -> Exists = lists:keymember(variable(Mapping), #mapping.variable, ListOfMappings), case Exists of true -> lists:keyreplace(variable(Mapping), #mapping.variable, ListOfMappings, Mapping); _ -> [Mapping | ListOfMappings] end. -spec remove_all_but_first(string(), [mapping()]) -> [mapping()]. remove_all_but_first(MappingName, Mappings) -> lists:foldr( fun(#mapping{mapping=MN}=M, Acc) when MN =:= MappingName-> [M|lists:keydelete(MN, #mapping.mapping, Acc)]; (M, Acc) -> [M|Acc] end, [], Mappings). -spec parse_collect(term()) -> collection_type() | undefined | cuttlefish_error:error(). parse_collect(undefined) -> undefined; parse_collect(list) -> list; parse_collect({map, atom}) -> {map, atom}; parse_collect({map, binary}) -> {map, binary}; parse_collect({proplist, atom}) -> {proplist, atom}; %% If {proplist, binary} is given, reject it; use {map, binary} for binary keys. parse_collect({proplist, binary}) -> {error, {unsupported_collect_type, {proplist, binary}}}; parse_collect(Other) -> {error, {invalid_collect_type, Other}}. -ifdef(TEST). -define(XLATE(X), lists:flatten(cuttlefish_error:xlate(X))). mapping_test() -> SampleMapping = { mapping, "conf.key", "erlang.key", [ {level, advanced}, {default, "default value"}, {datatype, {enum, [on, off]}}, {commented, "commented value"}, {include_default, "default_substitution"}, {new_conf_value, "config_file_val"}, {doc, ["documentation", "for feature"]}, {validators, ["valid.the.impailer"]}, hidden ] }, Record = parse(SampleMapping), ?assertEqual(["conf","key"], Record#mapping.variable), ?assertEqual("default value", Record#mapping.default), ?assertEqual("erlang.key", Record#mapping.mapping), ?assertEqual(advanced, Record#mapping.level), ?assertEqual([{enum, [on, off]}], Record#mapping.datatype), ?assertEqual(["documentation", "for feature"], Record#mapping.doc), ?assertEqual("default_substitution", Record#mapping.include_default), ?assertEqual("config_file_val", Record#mapping.new_conf_value), ?assertEqual(["valid.the.impailer"], Record#mapping.validators), ?assertEqual(true, Record#mapping.hidden), %% function tests ?assertEqual(["conf","key"], variable(Record)), ?assertEqual("default value", default(Record)), ?assertEqual("erlang.key", mapping(Record)), ?assertEqual(advanced, level(Record)), ?assertEqual([{enum, [on, off]}], datatype(Record)), ?assertEqual(["documentation", "for feature"], doc(Record)), ?assertEqual("default_substitution", include_default(Record)), ?assertEqual("config_file_val", new_conf_value(Record)), ?assertEqual(["valid.the.impailer"], validators(Record)), ?assertEqual(true, hidden(Record)), ok. replace_test() -> Element1 = parse({ mapping, "conf.key18", "erlang.key4", [ {level, advanced}, {default, "default value"}, {datatype, {enum, [on, off]}}, {commented, "commented value"}, {include_default, "default_substitution"}, {new_conf_value, "conf_val18"}, {doc, ["documentation", "for feature"]} ] }), SampleMappings = [Element1, parse({ mapping, "conf.key", "erlang.key2", [ {level, advanced}, {default, "default value"}, {datatype, {enum, [on, off]}}, {commented, "commented value"}, {include_default, "default_substitution"}, {new_conf_value, "conf_val_A"}, {doc, ["documentation", "for feature"]} ] }) ], Override = parse({ mapping, "conf.key", "erlang.key", [ {level, advanced}, {default, "default value"}, {datatype, {enum, [on, off]}}, {commented, "commented value"}, {include_default, "default_substitution"}, {new_conf_value, "conf_val_B"}, {doc, ["documentation", "for feature"]} ] }), NewMappings = replace(Override, SampleMappings), ?assertEqual([Element1, Override], NewMappings), ok. validators_test() -> Validators = [ cuttlefish_validator:parse( {validator, "a", "a desc", fun(_X) -> true end} ), cuttlefish_validator:parse({ validator, "b", "b desc", fun(_X) -> true end }), cuttlefish_validator:parse({ validator, "c", "c desc", fun(_X) -> true end }) ], %% Hack for coverage [ begin Fun = cuttlefish_validator:func(V), ?assert(Fun(x)) end || V <- Validators], Mapping = parse({ mapping, "conf.key", "erlang.key1", [ {validators, ["a", "b"]} ] }), [A, B, _C] = Validators, ?assertEqual([A,B], validators(Mapping, Validators)), MappingWithMissingValidator = parse({ mapping, "conf.key", "erlang.key1", [ {validators, ["a", "d"]} %% There is no "d" ] }), ?assertEqual([A], validators(MappingWithMissingValidator, Validators)), ok. parse_and_merge_test() -> SampleMappings = [parse({ mapping, "conf.key", "erlang.key1", [ {level, advanced}, {default, "default value"}, {datatype, {enum, [on, off]}}, {commented, "commented value"}, {include_default, "default_substitution"}, {new_conf_value, "conf_val_A"}, {doc, ["documentation", "for feature"]}, hidden ] }), parse({ mapping, "conf.key2", "erlang.key2", [ {level, advanced}, {default, "default value"}, {datatype, {enum, [on, off]}}, {commented, "commented value"}, {include_default, "default_substitution"}, {new_conf_value, "conf_val_B"}, {doc, ["documentation", "for feature"]} ] }) ], NewMappings = parse_and_merge({mapping, "conf.key", "erlang.key3", [{hidden, false}]}, SampleMappings), ?assertEqual("erlang.key3", mapping(hd(NewMappings))), ?assertEqual(false, hidden(hd(NewMappings))), ok. smart_merge_test() -> OldM1 = parse({mapping, "thing.to.merge", "some.key", [ {default, 7}, {datatype, integer}, {doc, ["documentation", "for feature"]} ]}), OldM2 = parse({mapping, "thing.not.merged", "some.other_key", [{default, 6}, {datatype, integer}]}), OriginalMappings = [OldM1, OldM2], NewRawNoMergeMapping = {mapping, "thing.to.merge", "some.new_other_key", [{level, advanced}]}, [NewUnMergedMapping, OldM2] = parse_and_merge( NewRawNoMergeMapping, OriginalMappings), ?assertEqual(["thing", "to", "merge"], variable(NewUnMergedMapping)), ?assertEqual(undefined, default(NewUnMergedMapping)), ?assertEqual("some.new_other_key", mapping(NewUnMergedMapping)), ?assertEqual(advanced, level(NewUnMergedMapping)), ?assertEqual([string], datatype(NewUnMergedMapping)), ?assertEqual([], doc(NewUnMergedMapping)), ?assertEqual(undefined, include_default(NewUnMergedMapping)), ?assertEqual([], validators(NewUnMergedMapping)), NewRawMergeMapping = {mapping, "thing.to.merge", "some.new_key", [merge, {level, advanced}]}, [NewMergedMapping, OldM2] = NewMappings = parse_and_merge( NewRawMergeMapping, OriginalMappings), ?assertEqual(["thing", "to", "merge"], variable(NewMergedMapping)), ?assertEqual(7, default(NewMergedMapping)), ?assertEqual("some.new_key", mapping(NewMergedMapping)), ?assertEqual(advanced, level(NewMergedMapping)), ?assertEqual([integer], datatype(NewMergedMapping)), ?assertEqual(["documentation", "for feature"], doc(NewMergedMapping)), ?assertEqual(undefined, include_default(NewMergedMapping)), ?assertEqual([], validators(NewMergedMapping)), NewerRawMergeMapping = {mapping, "thing.to.merge", "some.third_key", [merge, {default, 42}]}, [NewerMergedMapping, OldM2] = parse_and_merge( NewerRawMergeMapping, NewMappings), ?assertEqual(["thing", "to", "merge"], variable(NewerMergedMapping)), ?assertEqual(42, default(NewerMergedMapping)), ?assertEqual("some.third_key", mapping(NewerMergedMapping)), ?assertEqual(advanced, level(NewerMergedMapping)), ?assertEqual([integer], datatype(NewerMergedMapping)), ?assertEqual(["documentation", "for feature"], doc(NewerMergedMapping)), ?assertEqual(undefined, include_default(NewerMergedMapping)), ?assertEqual([], validators(NewerMergedMapping)), ok. accidentally_used_strings_for_enums_test() -> Mapping = parse({ mapping, "conf.key2", "erlang.key2", [ {datatype, {enum, ["on", "off"]}} ] }), ?assertEqual([{enum, [on, off]}], cuttlefish_mapping:datatype(Mapping)), ok. parse_error_test() -> {ErrorAtom, ErrorTuple} = parse(not_a_raw_mapping), ?assertEqual(error, ErrorAtom), ?assertEqual( "Poorly formatted input to cuttlefish_mapping:parse/1 : not_a_raw_mapping", ?XLATE(ErrorTuple)), ok. is_mapping_test() -> ?assert(not(is_mapping(not_a_mapping))), M = parse({ mapping, "conf.key2", "erlang.key2", [ {datatype, {enum, ["on", "off"]}} ] }), ?assert(is_mapping(M)), ok. %% conf.key can be any integer or the atom undefined. extended_types_parse_test() -> Mapping = parse({ mapping, "conf.key", "erlang.key", [ {datatype, [integer, {atom, undefined}]} ] }), ?assertEqual([integer, {atom, undefined}], cuttlefish_mapping:datatype(Mapping)), ok. datatype_cannot_be_empty_list_test() -> Mapping = parse({ mapping, "conf.key", "erlang.key", [ {datatype, []} ] }), ?assertMatch({error, _}, Mapping), ok. %% Aliases tests aliases_parse_test() -> M = parse({mapping, "new.key", "app.setting", [ {aliases, ["old.key", "older.key"]} ]}), ?assertEqual([["old", "key"], ["older", "key"]], aliases(M)), ?assertEqual([{["old", "key"], undefined}, {["older", "key"], undefined}], aliases_with_messages(M)). aliases_omitted_test() -> M = parse({mapping, "new.key", "app.setting", []}), ?assertEqual([], aliases(M)), ?assertEqual([], aliases_with_messages(M)). aliases_empty_list_error_test() -> Result = parse({mapping, "a.b", "app.setting", [{aliases, []}]}), ?assertMatch({error, {aliases_empty, _}}, Result). aliases_self_error_test() -> Result = parse({mapping, "a.b", "app.setting", [{aliases, ["a.b"]}]}), ?assertMatch({error, {alias_is_self, _}}, Result). aliases_self_among_multiple_error_test() -> Result = parse({mapping, "a.b", "app.setting", [ {aliases, ["old.key", "a.b"]} ]}), ?assertMatch({error, {alias_is_self, _}}, Result). alias_singular_test() -> M = parse({mapping, "new.key", "app.setting", [ {alias, "old.key"} ]}), ?assertEqual([["old", "key"]], aliases(M)), ?assertEqual([{["old", "key"], undefined}], aliases_with_messages(M)). alias_singular_self_error_test() -> Result = parse({mapping, "a.b", "app.setting", [{alias, "a.b"}]}), ?assertMatch({error, {alias_is_self, _}}, Result). alias_and_aliases_both_set_error_test() -> Result = parse({mapping, "a.b", "app.setting", [ {alias, "old.key"}, {aliases, ["older.key"]} ]}), ?assertMatch({error, {alias_and_aliases_both_set, _}}, Result). aliases_bare_string_error_test() -> Result = parse({mapping, "a.b", "app.setting", [{aliases, "old.key"}]}), ?assertMatch({error, {aliases_is_bare_string, _}}, Result). alias_singular_not_a_string_error_test() -> Result = parse({mapping, "a.b", "app.setting", [{alias, 42}]}), ?assertMatch({error, {alias_not_a_string, "a.b", 42}}, Result). aliases_duplicate_error_test() -> Result = parse({mapping, "a.b", "app.setting", [ {aliases, ["old.key", "old.key"]} ]}), ?assertMatch({error, {aliases_contain_duplicates, _}}, Result). aliases_invalid_value_atom_error_test() -> Result = parse({mapping, "a.b", "app.setting", [{aliases, deprecated}]}), ?assertMatch({error, {aliases_invalid_value, "a.b", deprecated}}, Result). aliases_invalid_value_integer_error_test() -> Result = parse({mapping, "a.b", "app.setting", [{aliases, 42}]}), ?assertMatch({error, {aliases_invalid_value, "a.b", 42}}, Result). aliases_invalid_value_tuple_error_test() -> Result = parse({mapping, "a.b", "app.setting", [{aliases, {"a", "b"}}]}), ?assertMatch({error, {aliases_invalid_value, "a.b", {"a", "b"}}}, Result). aliases_fuzzy_error_test() -> Result = parse({mapping, "a.b", "app.setting", [ {aliases, ["old.$name.key"]} ]}), ?assertMatch({error, {fuzzy_alias_unsupported, _, _}}, Result). %% Alias with message parsing tests aliases_with_message_test() -> M = parse({mapping, "new.key", "app.setting", [ {aliases, [{"old.key", "renamed in 4.0"}]} ]}), ?assertEqual([["old", "key"]], aliases(M)), ?assertEqual([{["old", "key"], "renamed in 4.0"}], aliases_with_messages(M)). aliases_mixed_with_and_without_message_test() -> M = parse({mapping, "new.key", "app.setting", [ {aliases, ["old.key", {"older.key", "legacy name"}]} ]}), ?assertEqual([["old", "key"], ["older", "key"]], aliases(M)), ?assertEqual([{["old", "key"], undefined}, {["older", "key"], "legacy name"}], aliases_with_messages(M)). alias_singular_with_message_test() -> M = parse({mapping, "new.key", "app.setting", [ {alias, {"old.key", "consolidated in 4.0"}} ]}), ?assertEqual([["old", "key"]], aliases(M)), ?assertEqual([{["old", "key"], "consolidated in 4.0"}], aliases_with_messages(M)). aliases_with_message_self_error_test() -> Result = parse({mapping, "a.b", "app.setting", [ {aliases, [{"a.b", "msg"}]} ]}), ?assertMatch({error, {alias_is_self, _}}, Result). aliases_with_message_duplicate_error_test() -> Result = parse({mapping, "a.b", "app.setting", [ {aliases, [{"old.key", "msg1"}, {"old.key", "msg2"}]} ]}), ?assertMatch({error, {aliases_contain_duplicates, _}}, Result). aliases_with_message_fuzzy_error_test() -> Result = parse({mapping, "a.b", "app.setting", [ {aliases, [{"old.$name.key", "msg"}]} ]}), ?assertMatch({error, {fuzzy_alias_unsupported, _, _}}, Result). aliases_with_bad_message_type_error_test() -> Result = parse({mapping, "a.b", "app.setting", [ {aliases, [{"old.key", 42}]} ]}), ?assertMatch({error, {alias_not_a_string, "a.b", {"old.key", 42}}}, Result). aliases_tuple_not_string_error_test() -> Result = parse({mapping, "a.b", "app.setting", [ {aliases, [{42, "msg"}]} ]}), ?assertMatch({error, {alias_not_a_string, "a.b", {42, "msg"}}}, Result). %% Collect parse tests collect_omitted_test() -> M = parse({mapping, "prefix.$name", "app.key", [{datatype, integer}]}), ?assertEqual(undefined, collect(M)). collect_list_test() -> M = parse({mapping, "prefix.$name", "app.key", [ {datatype, integer}, {collect, list} ]}), ?assertEqual(list, collect(M)). collect_map_atom_test() -> M = parse({mapping, "prefix.$name", "app.key", [ {collect, {map, atom}} ]}), ?assertEqual({map, atom}, collect(M)). collect_map_binary_test() -> M = parse({mapping, "prefix.$name", "app.key", [ {collect, {map, binary}} ]}), ?assertEqual({map, binary}, collect(M)). collect_proplist_atom_test() -> M = parse({mapping, "prefix.$name", "app.key", [ {collect, {proplist, atom}} ]}), ?assertEqual({proplist, atom}, collect(M)). collect_invalid_type_test() -> Result = parse({mapping, "prefix.$name", "app.key", [ {collect, not_valid} ]}), ?assertMatch({error, {invalid_collect_type, not_valid}}, Result). collect_proplist_binary_unsupported_test() -> Result = parse({mapping, "prefix.$name", "app.key", [ {collect, {proplist, binary}} ]}), ?assertMatch({error, {unsupported_collect_type, {proplist, binary}}}, Result). collect_on_non_fuzzy_test() -> Result = parse({mapping, "no.wildcard", "app.key", [ {collect, list} ]}), ?assertMatch({error, {collect_on_non_fuzzy, "no.wildcard"}}, Result). collect_multi_wildcard_test() -> Result = parse({mapping, "a.$x.b.$y", "app.key", [ {collect, list} ]}), ?assertMatch({error, {collect_multi_wildcard, "a.$x.b.$y", 2}}, Result). collect_merge_inherits_test() -> OldM = parse({mapping, "p.$n", "app.x", [ {default, 1}, {datatype, integer}, {collect, list} ]}), NewRaw = {mapping, "p.$n", "app.x", [merge, {default, 2}]}, [Merged] = parse_and_merge(NewRaw, [OldM]), ?assertEqual(list, collect(Merged)), ?assertEqual(2, default(Merged)). collect_merge_replaces_test() -> OldM = parse({mapping, "p.$n", "app.x", [ {default, 1}, {datatype, integer}, {collect, list} ]}), NewRaw = {mapping, "p.$n", "app.x", [merge, {collect, {map, atom}}]}, [Merged] = parse_and_merge(NewRaw, [OldM]), ?assertEqual({map, atom}, collect(Merged)). aliases_merge_replaces_test() -> OldM = parse({mapping, "a.b", "app.x", [ {default, 1}, {datatype, integer}, {aliases, ["old.ab"]} ]}), NewRaw = {mapping, "a.b", "app.x", [merge, {aliases, ["older.ab"]}]}, [Merged] = parse_and_merge(NewRaw, [OldM]), ?assertEqual([["older", "ab"]], aliases(Merged)), ?assertEqual(1, default(Merged)). aliases_merge_inherits_test() -> OldM = parse({mapping, "a.b", "app.x", [ {default, 1}, {datatype, integer}, {aliases, ["old.ab"]} ]}), NewRaw = {mapping, "a.b", "app.x", [merge, {default, 2}]}, [Merged] = parse_and_merge(NewRaw, [OldM]), ?assertEqual([["old", "ab"]], aliases(Merged)), ?assertEqual(2, default(Merged)). aliases_merge_singular_replaces_test() -> OldM = parse({mapping, "a.b", "app.x", [ {default, 1}, {datatype, integer}, {aliases, ["old.ab"]} ]}), NewRaw = {mapping, "a.b", "app.x", [merge, {alias, "newer.ab"}]}, [Merged] = parse_and_merge(NewRaw, [OldM]), ?assertEqual([["newer", "ab"]], aliases(Merged)), ?assertEqual(1, default(Merged)). aliases_merge_empty_clears_test() -> OldM = parse({mapping, "a.b", "app.x", [ {default, 1}, {datatype, integer}, {aliases, ["old.ab"]} ]}), NewRaw = {mapping, "a.b", "app.x", [merge, {aliases, []}]}, [Merged] = parse_and_merge(NewRaw, [OldM]), ?assertEqual([], aliases(Merged)), ?assertEqual(1, default(Merged)), ?assertEqual([integer], datatype(Merged)). aliases_merge_with_message_test() -> OldM = parse({mapping, "a.b", "app.x", [ {default, 1}, {datatype, integer}, {aliases, ["old.ab"]} ]}), NewRaw = {mapping, "a.b", "app.x", [merge, {aliases, [{"old.ab", "renamed in 4.0"}]} ]}, [Merged] = parse_and_merge(NewRaw, [OldM]), ?assertEqual([["old", "ab"]], aliases(Merged)), ?assertEqual([{["old", "ab"], "renamed in 4.0"}], aliases_with_messages(Merged)), ?assertEqual(1, default(Merged)). -endif.