Merge remote-tracking branch 'canonical/next'
[sinan.git] / src / sin_config.erl
blob1b53907eec2bd4f61329601345dfbf68ff8f542d
1 %% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
2 %%%---------------------------------------------------------------------------
3 %%% @author Eric Merritt <ericbmerritt@gmail.com>
4 %%% @copyright 2006, 2011 Erlware
5 %%% @doc
6 %%% Key = any atom
7 %%% Specifiers [TaskName, ReleaseName, AppName] = any atom
8 %%% Entry is a Key, Specifier, Value tuple
9 %%% Values may be term
10 %%%
11 %%% Keys may exist multple times as long as the have different specifiers
12 %%% Specifiers may be wildcarded with the wildcard atom (‘*’)
13 %%%
14 %%% The tightest specifier binding wins so if we have a two entries with the
15 %%% same key, but different specifier values, the closest specfier match
16 %%% wins. for example
17 %%%
18 %%% {foo, bar, baz, bof, “My Value”}
19 %%% {foo, bar, baz, ‘*’, “Other Value”}
20 %%% {foo, bar, ‘*’, ‘*’, “Still Another”}
21 %%%
22 %%% and we ask for
23 %%%
24 %%% foo, bar, baz, bof, we should get “My Value”
25 %%%
26 %%% if we ask for
27 %%% foo, bar, baz, buzzard, we should get “Other Value”
28 %%%
29 %%% and if we ask for
30 %%%
31 %%% foo, bar, bid, buckaroo, we should git “Still Another”
32 %%%
33 %%% and if we ask for
34 %%% foo, something, else, again we should throw a not found exception
35 %%%
36 %%% An abstract storage and management piece for config related key value
37 %%% pairs.
38 %%% @end
39 %%%----------------------------------------------------------------------------
40 -module(sin_config).
42 %% API
43 -export([new/0,
44 new_from_file/2,
45 new_from_terms/2,
46 create_matcher/2,
47 get/2,
48 match/2,
49 match/3,
50 add/3,
51 add_all/2,
52 merge/2,
53 remove/2,
54 size/1,
55 has_key/2,
56 will_match/2,
57 to_list/1,
58 format_exception/1]).
60 %% Private API
61 -export(['__add__'/3,
62 '__match__'/2,
63 '__match__'/3,
64 '__will_match__'/2]).
66 -export_type([config/0,
67 matcher/0,
68 read_exception/0,
69 file_name/0,
70 full_key/0,
71 partial_entry/0,
72 spec_opts/0,
73 spec_name/0,
74 key/0,
75 value/0]).
77 -include_lib("eunit/include/eunit.hrl").
79 -define(WRAP(X), {?MODULE, X}).
81 %%====================================================================
82 %% Types
83 %%====================================================================
86 -opaque config() :: {config, list()}.
87 -type matcher() :: module().
88 -type read_exception() :: {error_accessing_file, string(), term()}.
89 -type file_name() :: string().
90 -type key() :: atom() | binary() | string().
91 -type specifier() :: '*' | atom().
92 -type full_key() :: {key(), specifier(),
93 specifier(), specifier(),
94 specifier, specifier()}.
95 -type spec_name() :: task | release | app | dir | module.
96 -type spec_opts() :: [{spec_name(), atom()}].
97 -type partial_key() :: key()
98 | {key(), specifier()}
99 | {key(), specifier(), specifier()}
100 | {key(), specifier(), specifier(), specifier()}
101 | {key(), specifier(), specifier(),
102 specifier(), specifier()}
103 | {key(), specifier(), specifier(), specifier(),
104 specifier(), specifier()}
105 | {key(), spec_opts()}.
107 -type partial_entry() :: {partial_key(), value()}.
109 -type value() :: term().
110 -type internal_config_entry() :: {full_key(), value()}.
112 %%====================================================================
113 %% API
114 %%====================================================================
116 %% @doc Create a new empty config
117 -spec new() -> config().
118 new() ->
119 ?WRAP([]).
121 -spec new_from_file(file_name(), spec_opts()) ->
122 config().
123 new_from_file(ConfigFile, Opts) ->
124 Terms = read_config(ConfigFile),
125 new_from_terms(Terms, Opts).
127 %% @doc Create a new config from a config file
128 -spec new_from_terms([partial_entry()], spec_opts()) -> config().
129 new_from_terms(Terms, Opts) when is_list(Terms)->
130 Interim = lists:foldl(fun(Term, Config) ->
131 [process_term(Term, Opts) | Config]
132 end, [], Terms),
133 ?WRAP(sort(Interim)).
135 %% @doc Create a matcher that has all the specified defaults, specified by spec
136 %% opts
137 -spec create_matcher(spec_opts(), config()) ->
138 matcher().
139 create_matcher(Opts0, Config)
140 when is_list(Opts0)->
141 Task = get_opt(task, Opts0),
142 Release = get_opt(release, Opts0),
143 App = get_opt(app, Opts0),
144 Dir = get_opt(dir, Opts0),
145 Module = get_opt(module, Opts0),
147 sin_matcher:new(Config, Opts0, Task, Release,
148 App, Dir, Module).
150 %% @doc add a new key value pair to the config, if the key is an exact match
151 %% with an existing key the existing key is replaced.
152 -spec add(partial_key(), value(), config()) -> config().
153 add(Key0, Value, Config) ->
154 Key1 = process_key(Key0, []),
155 '__add__'(Key1, Value, Config).
157 %% @doc internal version of add
158 '__add__'(Key0, Value, ?WRAP(Config)) ->
159 {NewConfig, Replaced} =
160 lists:foldl(fun(Entry = {Key, _}, {NewConfig, Replaced}) ->
161 case exact_match(Key0, Key) of
162 true ->
163 {[{Key, Value} | NewConfig], true};
164 false ->
165 {[Entry | NewConfig], Replaced}
167 end,
168 {[], false},
169 Config),
170 case Replaced of
171 false ->
172 ?WRAP(sort([{Key0, Value} | Config]));
173 true ->
174 ?WRAP(sort(NewConfig))
175 end.
177 %% @doc simply loop over key value pairs adding as it goes
178 -spec add_all([{full_key(), value()}], config()) -> config().
179 add_all(KeyValuePairs, Config) ->
180 lists:foldl(fun({InKey, Value}, NewConfig) ->
181 add(InKey, Value, NewConfig)
182 end, Config, KeyValuePairs).
184 %% @doc two configs together with the first argument taking precidence over the
185 %% second
186 -spec merge(config(), config()) -> config().
187 merge(?WRAP(Config1), Config2) ->
188 lists:foldl(fun({InKey, Value}, NewConfig) ->
189 '__add__'(InKey, Value, NewConfig)
190 end, Config2, Config1).
192 %% @doc get a value from the key where.
193 -spec get(full_key(), config()) -> value().
194 get(Key, ?WRAP(Values))
195 when erlang:is_tuple(Key), erlang:size(Key) == 6 ->
196 {_, Value} = ec_lists:fetch(fun({TKey, _}) ->
197 exact_match(TKey, Key)
198 end, Values),
199 Value.
201 %% @doc get a value from the key where.
202 -spec match(partial_key(), config()) -> value().
203 match(Key0, Values) ->
204 Key1 = process_key(Key0, []),
205 '__match__'(Key1, Values).
207 %% @doc non key manipulated version of match
208 -spec '__match__'(full_key(), config()) -> value().
209 '__match__'(Key0, ?WRAP(Values)) ->
210 {_, Value} = ec_lists:fetch(fun({TKey, _}) ->
211 inexact_match(TKey, Key0)
212 end, Values),
213 Value.
215 %% @doc get a value from the key where.
216 -spec match(partial_key(), value(), config()) -> value().
217 match(Key0, Default, Values) ->
218 Key1 = process_key(Key0, []),
219 '__match__'(Key1, Default, Values).
221 %% @doc internal non-key manipulated version of match/3
222 -spec '__match__'(full_key(), value(), config()) -> value().
223 '__match__'(Key0, Default, ?WRAP(Values))
224 when is_tuple(Key0), erlang:size(Key0) == 6 ->
226 {_, Value} = ec_lists:fetch(fun({TKey, _}) ->
227 inexact_match(TKey, Key0)
228 end, Values),
229 Value
230 catch
231 not_found ->
232 Default
233 end.
235 %% @doc remove the config entry by an exact match of keys
236 -spec remove(full_key(), config()) -> config().
237 remove(Key, ?WRAP(Config)) ->
238 ?WRAP(sort(lists:filter(fun({TKey, _Value}) ->
239 not exact_match(TKey, Key)
240 end,
241 Config))).
243 %% @doc get the current number of entries in the config
244 -spec size(config()) -> non_neg_integer().
245 size(?WRAP(Config)) ->
246 erlang:length(Config).
248 %% @doc test to see if the key exists in the data base via an exact match
249 -spec has_key(full_key(), config()) -> boolean().
250 has_key(Key, ?WRAP(Config))
251 when erlang:is_tuple(Key), erlang:size(Key) == 6 ->
252 case ec_lists:find(fun({TKey, _}) ->
253 exact_match(TKey, Key)
254 end, Config) of
255 {ok, _} ->
256 true;
257 _ ->
258 false
259 end.
261 %% @doc test to see if the key exists in the data base via an exact match
262 -spec will_match(partial_key(), config()) -> boolean().
263 will_match(Key0, Config) ->
264 Key1 = process_key(Key0, []),
265 '__will_match__'(Key1, Config).
267 %% @doc internal non-key manipulated version of will match
268 -spec '__will_match__'(full_key(), config()) -> boolean().
269 '__will_match__'(Key, ?WRAP(Config))
270 when erlang:is_tuple(Key), erlang:size(Key) == 6 ->
271 case ec_lists:find(fun({TKey, _}) ->
272 inexact_match(TKey, Key)
273 end, Config) of
274 {ok, _} ->
275 true;
276 _ ->
277 false
278 end.
280 to_list(?WRAP(Config)) ->
281 Config.
283 %% @doc Format an exception thrown by this module
284 -spec format_exception(sin_exceptions:exception()) ->
285 string().
286 format_exception(Exception) ->
287 sin_exceptions:format_exception(Exception).
289 %%====================================================================
290 %% Internal Functions
291 %%====================================================================
293 %% @doc read in the config file using file:consult
294 -spec read_config(file_name()) -> [term()].
295 read_config(ConfigFile) ->
296 case file:consult(ConfigFile) of
297 {ok, Terms} ->
298 Terms;
299 {error, Reason} ->
300 throw({error_accessing_file, ConfigFile, Reason})
301 end.
303 %% @doc convert the user entered value into an expanded full value tuple
304 -spec process_term(partial_entry(), spec_opts()) ->
305 internal_config_entry().
306 process_term({Key, Value}, Opts) ->
307 {process_key(Key, Opts), Value};
308 process_term({Key, SpecOpts, Value}, Opts) ->
309 {process_key(Key, SpecOpts ++ Opts), Value}.
311 %% @doc convert the user entered key into an expanded full key tuple
312 -spec process_key(partial_entry(), spec_opts()) ->
313 full_key().
314 process_key({Key, UserOpts = [{A, V} | _]}, Opts)
315 when is_atom(A), is_atom(V) ->
316 NewOpts = UserOpts ++ Opts,
317 {Key,
318 get_opt(task, NewOpts),
319 get_opt(release, NewOpts),
320 get_opt(app, NewOpts),
321 get_opt(dir, NewOpts),
322 get_opt(module, NewOpts)};
323 process_key(Key, Opts) ->
324 {Key,
325 get_opt(task, Opts),
326 get_opt(release, Opts),
327 get_opt(app, Opts),
328 get_opt(dir, Opts),
329 get_opt(module, Opts)}.
332 %% @doc given a spec list and a spec name return the value from the
333 -spec get_opt(spec_name(), spec_opts()) -> '*' | atom().
334 get_opt(OptName, Opts) ->
335 case lists:keyfind(OptName, 1, Opts) of
336 {OptName, Value} ->
337 Value;
338 false ->
340 end.
342 equality([{'*', '*'} | Rest]) ->
343 equality(Rest);
344 equality([{'*', _B} | _Rest]) ->
345 false;
346 equality([{_A, '*'} | Rest]) ->
347 equality(Rest);
348 equality([{A, B} | Rest])
349 when A == B ->
350 equality(Rest);
351 equality([{A, B} | _Rest])
352 when A < B ->
353 false;
354 equality([{A, B} | _Rest])
355 when A > B ->
356 true;
357 equality([]) ->
358 true.
360 sort(Config) ->
361 lists:sort(fun({{AK, AS1, AS2, AS3, AS4, AS5}, _},
362 {{BK, BS1, BS2, BS3, BS4, BS5}, _}) ->
363 equality([{AK, BK}, {AS1, BS1}, {AS2, BS2},
364 {AS3, BS3}, {AS4, BS4}, {AS5, BS5}])
365 end, Config).
367 %% @doc allow a loose matching where wildcards in K1 will match against any
368 %% value in K2
369 -spec inexact_match(K1::full_key(), K2::full_key()) -> boolean().
370 inexact_match({Key, TaskName, ReleaseName, AppName, DirName, ModName},
371 {Key, UTaskName, UReleaseName, UAppName, UDirName, UModName})
372 when (TaskName == '*' orelse TaskName == UTaskName) andalso
373 (ReleaseName == '*' orelse ReleaseName == UReleaseName) andalso
374 (AppName == '*' orelse AppName == UAppName) andalso
375 (DirName == '*' orelse DirName == UDirName) andalso
376 (ModName == '*' orelse ModName == UModName) ->
377 true;
378 inexact_match(_IncomingKey, _Key) ->
379 false.
381 %% @doc make sure the two keys match exactly
382 -spec exact_match(full_key(), full_key()) -> boolean().
383 exact_match(K1, K1) ->
384 true;
385 exact_match(_IncomingKey, _Key) ->
386 false.