Current section

Files

Jump to
cuttlefish src cuttlefish_mapping.erl
Raw

src/cuttlefish_mapping.erl

%% -------------------------------------------------------------------
%%
%% 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,
set_datatype/2,
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.
%% Pipeline-internal: swaps the datatype list in place so a later
%% stage can substitute resolved `{validator, Fun}' for the parsed
%% `{validator, Name}' entries. Not intended for schema authors.
-spec set_datatype(mapping(), cuttlefish_datatypes:datatype_list()) -> mapping().
set_datatype(M, DT) -> M#mapping{datatype = DT}.
-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 find_validator(VName, Validators) of
false -> Vs;
V -> [V|Vs]
end
end, [], M#mapping.validators).
%% Look up by canonical name first, then by alias. Canonical match
%% wins so a validator named "X" beats an alias "X" on a different
%% validator — the same precedence rule mapping aliases use.
find_validator(Name, Validators) ->
case lists:keyfind(Name, 2, Validators) of
false ->
find_by_alias(Name, Validators);
V -> V
end.
find_by_alias(_Name, []) -> false;
find_by_alias(Name, [V | Rest]) ->
case lists:member(Name, cuttlefish_validator:aliases(V)) of
true -> V;
false -> find_by_alias(Name, Rest)
end.
-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.