1 %% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
2 %%%---------------------------------------------------------------------------
3 %%% @author Eric Merritt <ericbmerritt@gmail.com>
4 %%% @copyright 2006, 2011 Erlware
7 %%% Specifiers [TaskName, ReleaseName, AppName] = any atom
8 %%% Entry is a Key, Specifier, Value tuple
11 %%% Keys may exist multple times as long as the have different specifiers
12 %%% Specifiers may be wildcarded with the wildcard atom (‘*’)
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
18 %%% {foo, bar, baz, bof, “My Value”}
19 %%% {foo, bar, baz, ‘*’, “Other Value”}
20 %%% {foo, bar, ‘*’, ‘*’, “Still Another”}
24 %%% foo, bar, baz, bof, we should get “My Value”
27 %%% foo, bar, baz, buzzard, we should get “Other Value”
31 %%% foo, bar, bid, buckaroo, we should git “Still Another”
34 %%% foo, something, else, again we should throw a not found exception
36 %%% An abstract storage and management piece for config related key value
39 %%%----------------------------------------------------------------------------
66 -export_type([config
/0,
77 -include_lib("eunit/include/eunit.hrl").
79 -define(WRAP(X
), {?MODULE
, X
}).
81 %%====================================================================
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 %%====================================================================
114 %%====================================================================
116 %% @doc Create a new empty config
117 -spec
new() -> config().
121 -spec
new_from_file(file_name(), spec_opts()) ->
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
]
133 ?
WRAP(sort(Interim
)).
135 %% @doc Create a matcher that has all the specified defaults, specified by spec
137 -spec
create_matcher(spec_opts(), config()) ->
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
,
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
163 {[{Key
, Value
} | NewConfig
], true
};
165 {[Entry
| NewConfig
], Replaced
}
172 ?
WRAP(sort([{Key0
, Value
} | Config
]));
174 ?
WRAP(sort(NewConfig
))
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
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
)
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
)
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
)
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
)
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
)
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
)
280 to_list(?
WRAP(Config
)) ->
283 %% @doc Format an exception thrown by this module
284 -spec
format_exception(sin_exceptions:exception()) ->
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
300 throw({error_accessing_file
, ConfigFile
, Reason
})
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()) ->
314 process_key({Key
, UserOpts
= [{A
, V
} | _
]}, Opts
)
315 when is_atom(A
), is_atom(V
) ->
316 NewOpts
= UserOpts
++ Opts
,
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
) ->
326 get_opt(release
, 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
342 equality([{'*', '*'} | Rest
]) ->
344 equality([{'*', _B
} | _Rest
]) ->
346 equality([{_A
, '*'} | Rest
]) ->
348 equality([{A
, B
} | Rest
])
351 equality([{A
, B
} | _Rest
])
354 equality([{A
, B
} | _Rest
])
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
}])
367 %% @doc allow a loose matching where wildcards in K1 will match against any
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
) ->
378 inexact_match(_IncomingKey
, _Key
) ->
381 %% @doc make sure the two keys match exactly
382 -spec
exact_match(full_key(), full_key()) -> boolean().
383 exact_match(K1
, K1
) ->
385 exact_match(_IncomingKey
, _Key
) ->