Current section
Files
Jump to
Current section
Files
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.
-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()
}).
-type mapping() :: #mapping{}.
-type raw_mapping() :: {mapping, string(), string(), [proplists:property()]}.
-export_type([mapping/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
]).
-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,
case Datatype of
{error, _} ->
Datatype;
_ ->
#mapping{
variable = cuttlefish_variable:tokenize(Variable),
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)
}
end;
parse(X) ->
{error, {mapping_parse, X}}.
%% 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()].
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,
lists:keyreplace(Var, #mapping.variable, Mappings, MaybeMergedMapping)
end.
-spec merge(raw_mapping(), mapping()) -> mapping().
merge(NewMappingSource, OldMapping) ->
MergeMapping = parse(NewMappingSource),
#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(include_default, NewMappingSource, MergeMapping, OldMapping),
validators = choose(validators, NewMappingSource, MergeMapping, OldMapping),
see = choose(see, NewMappingSource, MergeMapping, OldMapping),
hidden = choose(hidden, NewMappingSource, MergeMapping, OldMapping)
}.
choose(Field, {_, _, _, PreParseMergeProps}, MergeMapping, OldMapping) ->
Which = case {Field,
proplists:is_defined(Field, PreParseMergeProps),
proplists:get_value(Field, PreParseMergeProps)} of
{see, _, []} -> old;
{doc, _, []} -> old;
{_, true, _} -> new;
_ -> old
end,
case Which of
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 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).
-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.
-endif.