├── .gitignore
├── .travis.yml
├── LICENSE
├── NEWS.md
├── NOTICE
├── README.md
├── doc
├── README.md
├── econfig.md
├── edoc-info
├── erlang.png
├── overview.edoc
└── stylesheet.css
├── rebar.config
├── rebar.lock
├── src
├── econfig.app.src
├── econfig.erl
├── econfig_app.erl
├── econfig_file_writer.erl
├── econfig_server.erl
├── econfig_sup.erl
├── econfig_util.erl
├── econfig_watcher.erl
└── econfig_watcher_sup.erl
└── test
├── bootstrap_travis.sh
└── fixtures
├── test.ini
└── test2.ini
/.gitignore:
--------------------------------------------------------------------------------
1 | .rebar3
2 | _*
3 | .eunit
4 | *.o
5 | *.beam
6 | *.plt
7 | *.swp
8 | *.swo
9 | .erlang.cookie
10 | ebin
11 | log
12 | erl_crash.dump
13 | .rebar
14 | _rel
15 | _deps
16 | _plugins
17 | _tdeps
18 | logs
19 | _build
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: erlang
2 | otp_release:
3 | - R16B03-1
4 | - 17.0
5 | - 17.1
6 | - 18.0
7 | - 18.1
8 | before_script:
9 | - "./test/bootstrap_travis.sh"
10 | script: "./rebar3 eunit"
11 | notifications:
12 | irc:
13 | - "irc.freenode.org#refuge.io"
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | 2012-2016 (c) Benoît Chesneau
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
15 |
--------------------------------------------------------------------------------
/NEWS.md:
--------------------------------------------------------------------------------
1 | #econfig news
2 |
3 | 0.7.1 - 2015-11-08
4 | ------------------
5 |
6 | - fix bad case clause with config:get_list/4
7 | - fix bad case clause with {M, F} case in change_fun.
8 |
9 | 0.7.0 - 2015-11-08
10 | ------------------
11 |
12 | - fix notifications. Only exposes needed events and add tests
13 |
14 | 0.6.2 - 2015-11-08
15 | ------------------
16 |
17 | - register econfig_sup process.
18 | - fix doc
19 |
20 | 0.6.1 - 2015-11-07
21 | ------------------
22 |
23 | - convert a float value to string when setting it
24 | - allows to set a boolean value
25 | - safely run the change hook, ignore errors, just report them
26 |
27 | 0.6.0 - 2015-11-07
28 | ------------------
29 |
30 | - add helpers functions
31 |
32 | 0.5.0 - 2015-11-07
33 | ------------------
34 |
35 | - add `on_change` function
36 | - remove gproc dependency
37 | - move to rebar3 + hex packaging support
38 | - make ets resilient to the main process crash
39 |
40 | 0.4.2 - 2014-03-22
41 | ------------------
42 |
43 | - add: basic unitests
44 | - fix: trim whitespaces from key and value
45 |
46 | 0.4.1 - 2013-06-20
47 | ------------------
48 |
49 | - add `econfig:set_value/3`: function to fill a complete section with a
50 | proplists.
51 | - add `econfig:delete_value/2`: function to delete all the keys in a
52 | section.
53 |
54 | 0.4 - 2013-06-22
55 | ----------------
56 |
57 | - add econfig:start/0 and econfig:stop/0 functions to start and stop easily econfig in tests or on the shell.
58 | - add `econfig:section/1` function to get a list of all sections.
59 | - add `econfig:prefix` function to get al l sections starting with Prefix
60 | - add `econfig:cfg2list/2` to retrieve all the configuration as a proplist
61 | - add `econfig:cfg2list/3` to retrieves all the config as a proplist and group sections by key:
62 |
63 | 0.3 - 2012-05-18
64 | ----------------
65 |
66 | - add `{autoreload, ScanDelay}` to `register_config/3`
67 | - advertise the reload event and describe the updates types.
68 |
69 | 0.2 - 2012-04-30
70 | ----------------
71 |
72 | - add the possibility to initialize econfig with defaults configs.
73 | - improve files changes handling: don't reload when a value is
74 | updated from econfig
75 | - fix config dirs handling
76 |
77 | 0.1 - 2012-04-29
78 | ----------------
79 |
80 | - Initial release
81 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | econfig
2 | -------
3 |
4 | 2012-2016 (c) Benoît Chesneau
5 | 2012 (c) The Apache CouchDB project
6 |
7 | econfig is released under the Apache 2 license. See the LICENSE file for
8 | the complete license.
9 |
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # econfig - simple Erlang config handler using INI files #
4 |
5 | Copyright (c) 2012-2016 Benoît Chesneau.
6 |
7 | __Version:__ 0.7.3
8 |
9 | # econfig
10 |
11 | econfig is a simple Erlang config handler to manage a config from INI
12 | files.
13 |
14 | [](https://travis-ci.org/benoitc/econfig)
15 | [](https://hex.pm/packages/econfig)
16 |
17 | econfig can be use to read and update INI files. Values are cached in an
18 | ETS table and you can manage multiple configuration profiles. A process
19 | can also subscribe to config updates events.
20 |
21 | Autoreload of the config when an INI file is updated is supported, you can even
22 | manage changes from a full config directory.
23 |
24 | See the [NEWS](http://github.com/benoitc/econfig/blob/master/NEWS.md)
25 | for last changes.
26 |
27 | #### Useful modules are:
28 |
29 | - [`econfig`](http://github.com/benoitc/econfig/blob/master/doc/econfig.md): main module. It contains all the API.
30 |
31 | ## Examples
32 |
33 | Quick usage example:
34 |
35 | ```
36 | 1> application:ensure_all_started(econfig).
37 | ok
38 | 2> econfig:register_config(test, ["test/fixtures/test.ini", "test/fixtures/test2.ini"], [autoreload]).
39 | ok
40 | 3> econfig:subscribe(test).
41 | true
42 | 4> econfig:get_value(test, "section1").
43 | [{"key 3","value 3"},
44 | {"key1","value1"},
45 | {"key2","value 2"},
46 | {"key4","value 4"},
47 | {"key5","value5"}]
48 | 5> econfig:set_value(test, "section1", "key6", "value6").
49 | ok
50 | 6> flush().
51 | Shell got {config_updated,test,{set,{"section1","key6"}}}
52 | ok
53 | ```
54 |
55 | ## Advanced features
56 |
57 | ### on_change hook
58 |
59 | Some application may want to handle changes without suscribing to change. This change allows a user to pass a change function when registering the configuation. This function will be called each time a change happen.
60 |
61 | ### helpers functions
62 |
63 | econfig do not guess datatypes of values in configuration files, always storing them internally as strings. This means that if you need other datatypes, you should convert on your own. Some helpers are provided to do it:
64 |
65 | - `econfig:get_boolean/{3, 4}`: to convert to boolean
66 | - `econfig:get_integer/{3, 4}`: to convert to integer
67 | - `econfig:get_float/{3, 4}`: to convert to float
68 | - `econfig:get_list/{3, 4}`: to convert a list of string separated by `,` to a list.
69 | - `econfig:get_binary/{3, 4}`: to convert to a binary
70 |
71 | Contribute
72 | ----------
73 | For issues, comments or feedback please [create an issue!] [1]
74 |
75 | [1]: http://github.com/benoitc/econfig/issues "econfig issues"
76 |
--------------------------------------------------------------------------------
/doc/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # econfig - simple Erlang config handler using INI files #
4 |
5 | Copyright (c) 2012-2016 Benoît Chesneau.
6 |
7 | __Version:__ 0.7.3
8 |
9 | # econfig
10 |
11 | econfig is a simple Erlang config handler to manage a config from INI
12 | files.
13 |
14 | [](https://travis-ci.org/benoitc/econfig)
15 | [](https://hex.pm/packages/econfig)
16 |
17 | econfig can be use to read and update INI files. Values are cached in an
18 | ETS table and you can manage multiple configuration profiles. A process
19 | can also subscribe to config updates events.
20 |
21 | Autoreload of the config when an INI file is updated is supported, you can even
22 | manage changes from a full config directory.
23 |
24 | See the [NEWS](http://github.com/benoitc/econfig/blob/master/NEWS.md)
25 | for last changes.
26 |
27 | #### Useful modules are:
28 |
29 | - [`econfig`](econfig.md): main module. It contains all the API.
30 |
31 | ## Examples
32 |
33 | Quick usage example:
34 |
35 | ```
36 | 1> application:ensure_all_started(econfig).
37 | ok
38 | 2> econfig:register_config(test, ["test/fixtures/test.ini", "test/fixtures/test2.ini"], [autoreload]).
39 | ok
40 | 3> econfig:subscribe(test).
41 | true
42 | 4> econfig:get_value(test, "section1").
43 | [{"key 3","value 3"},
44 | {"key1","value1"},
45 | {"key2","value 2"},
46 | {"key4","value 4"},
47 | {"key5","value5"}]
48 | 5> econfig:set_value(test, "section1", "key6", "value6").
49 | ok
50 | 6> flush().
51 | Shell got {config_updated,test,{set,{"section1","key6"}}}
52 | ok
53 | ```
54 |
55 | ## Advanced features
56 |
57 | ### on_change hook
58 |
59 | Some application may want to handle changes without suscribing to change. This change allows a user to pass a change function when registering the configuation. This function will be called each time a change happen.
60 |
61 | ### helpers functions
62 |
63 | econfig do not guess datatypes of values in configuration files, always storing them internally as strings. This means that if you need other datatypes, you should convert on your own. Some helpers are provided to do it:
64 |
65 | - `econfig:get_boolean/{3, 4}`: to convert to boolean
66 | - `econfig:get_integer/{3, 4}`: to convert to integer
67 | - `econfig:get_float/{3, 4}`: to convert to float
68 | - `econfig:get_list/{3, 4}`: to convert a list of string separated by `,` to a list.
69 | - `econfig:get_binary/{3, 4}`: to convert to a binary
70 |
71 | Contribute
72 | ----------
73 | For issues, comments or feedback please [create an issue!] [1]
74 |
75 | [1]: http://github.com/benoitc/econfig/issues "econfig issues"
76 |
--------------------------------------------------------------------------------
/doc/econfig.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Module econfig #
4 | * [Description](#description)
5 | * [Data Types](#types)
6 | * [Function Index](#index)
7 | * [Function Details](#functions)
8 |
9 | Public API of econfig
10 | econfig rely on a central gen_server an an ETS ordered-set.
11 |
12 |
13 |
14 | ## Data Types ##
15 |
16 |
17 |
18 |
19 | ### config_name() ###
20 |
21 |
22 |
23 | config_name() = atom() | string() | binary()
24 |
25 |
26 |
27 |
28 |
29 | ### config_options() ###
30 |
31 |
32 |
33 | config_options() = [autoreload | {autoreload, integer} | {change_fun, function()}]
34 |
35 |
36 |
37 |
38 |
39 | ### inifile() ###
40 |
41 |
42 |
43 | inifile() = string()
44 |
45 |
46 |
47 |
48 |
49 | ### inifiles() ###
50 |
51 |
52 |
53 | inifiles() = [inifile()]
54 |
55 |
56 |
57 |
58 |
59 | ### kvs() ###
60 |
61 |
62 |
63 | kvs() = [{any(), any()}]
64 |
65 |
66 |
67 |
68 |
69 | ### section() ###
70 |
71 |
72 |
73 | section() = string()
74 |
75 |
76 |
77 |
78 | ## Function Index ##
79 |
80 |
81 |
86 |
87 |
88 |
89 |
90 | ## Function Details ##
91 |
92 |
93 |
94 | ### all/1 ###
95 |
96 |
97 | all(ConfigName::config_name()) -> [{section(), [{string(), string()}]}]
98 |
99 |
100 |
101 | get all values of a configuration
102 |
103 |
104 |
105 | ### cfg2list/1 ###
106 |
107 |
108 | cfg2list(ConfigName::config_name()) -> [{section(), [{string(), string()}]}]
109 |
110 |
111 |
112 | retrive config as a proplist
113 |
114 |
115 |
116 | ### cfg2list/2 ###
117 |
118 |
119 | cfg2list(ConfigName::config_name(), GroupKey::string()) -> [{section(), [{string(), string()}]}]
120 |
121 |
122 |
123 | retrieve config as a proplist
124 |
125 |
126 |
127 | ### delete_value/2 ###
128 |
129 |
130 | delete_value(ConfigName::config_name(), Section::section()) -> ok
131 |
132 |
133 |
134 | delete all key/values from a section
135 |
136 |
137 |
138 | ### delete_value/3 ###
139 |
140 |
141 | delete_value(ConfigName::config_name(), Section::section(), Key::any()) -> ok
142 |
143 |
144 |
145 | delete a value and persist the change to the file
146 |
147 |
148 |
149 | ### delete_value/4 ###
150 |
151 |
152 | delete_value(ConfigName::config_name(), Section::section(), Key::any(), Persist::boolean()) -> ok
153 |
154 |
155 |
156 | delete a value and optionnally persist it
157 |
158 |
159 |
160 | ### get_binary/3 ###
161 |
162 |
163 | get_binary(ConfigName::config_name(), Section::section(), Key::any()) -> Value::binary() | undefined
164 |
165 |
166 |
167 | get a value and convert it to an binary
168 |
169 |
170 |
171 | ### get_binary/4 ###
172 |
173 |
174 | get_binary(ConfigName::config_name(), Section::section(), Key::any(), Default::binary()) -> Value::binary()
175 |
176 |
177 |
178 | get a value and convert it to an binary
179 |
180 |
181 |
182 | ### get_boolean/3 ###
183 |
184 |
185 | get_boolean(ConfigName::config_name(), Section::section(), Key::any()) -> Value::boolean() | undefined
186 |
187 |
188 |
189 | get a value and convert it to a boolean if possible
190 | This method is case-insensitive and recognizes Boolean values from 'yes'/'no', 'on'/'off', 'true'/'false' and '1'/'0'
191 | a badarg error is raised if the value can't be parsed to a boolean
192 |
193 |
194 |
195 | ### get_boolean/4 ###
196 |
197 |
198 | get_boolean(ConfigName::config_name(), Section::section(), Key::any(), Default::boolean()) -> Value::boolean()
199 |
200 |
201 |
202 | get a value and convert it to a boolean if possible. It fallback to default if not set.
203 | This method is case-insensitive and recognizes Boolean values from 'yes'/'no', 'on'/'off', 'true'/'false' and '1'/'0'
204 | a badarg error is raised if the value can't be parsed to a boolean
205 |
206 |
207 |
208 | ### get_float/3 ###
209 |
210 |
211 | get_float(ConfigName::config_name(), Section::section(), Key::any()) -> Value::float() | undefined
212 |
213 |
214 |
215 | get a value and convert it to an float
216 |
217 |
218 |
219 | ### get_float/4 ###
220 |
221 |
222 | get_float(ConfigName::config_name(), Section::section(), Key::any(), Default::float()) -> Value::float()
223 |
224 |
225 |
226 | get a value and convert it to an float
227 |
228 |
229 |
230 | ### get_integer/3 ###
231 |
232 |
233 | get_integer(ConfigName::config_name(), Section::section(), Key::any()) -> Value::integer() | undefined
234 |
235 |
236 |
237 | get a value and convert it to an integer
238 |
239 |
240 |
241 | ### get_integer/4 ###
242 |
243 |
244 | get_integer(ConfigName::config_name(), Section::section(), Key::any(), Default::integer()) -> Value::integer()
245 |
246 |
247 |
248 | get a value and convert it to an integer
249 |
250 |
251 |
252 | ### get_list/3 ###
253 |
254 |
255 | get_list(ConfigName::config_name(), Section::section(), Key::any()) -> Value::list() | undefined
256 |
257 |
258 |
259 | get a value and convert it to an list
260 |
261 |
262 |
263 | ### get_list/4 ###
264 |
265 |
266 | get_list(ConfigName::config_name(), Section::section(), Key::any(), Default::list()) -> Value::list()
267 |
268 |
269 |
270 | get a value and convert it to an list
271 |
272 |
273 |
274 | ### get_value/2 ###
275 |
276 |
277 | get_value(ConfigName::config_name(), Section::string()) -> [{string(), string()}]
278 |
279 |
280 |
281 | get keys/values of a section
282 |
283 |
284 |
285 | ### get_value/3 ###
286 |
287 |
288 | get_value(ConfigName::config_name(), Section::section(), Key::any()) -> Value::string() | undefined
289 |
290 |
291 |
292 | get value for a key in a section
293 |
294 |
295 |
296 | ### get_value/4 ###
297 |
298 |
299 | get_value(ConfigName::config_name(), Section::section(), Key::any(), Default::any()) -> Value::string()
300 |
301 |
302 |
303 | get value for a key in a section or return the default value if not set
304 |
305 |
306 |
307 | ### open_config/2 ###
308 |
309 |
310 | open_config(ConfigName::config_name(), IniFiles::inifiles()) -> ok | {error, any()}
311 |
312 |
313 |
314 | open or create an ini file an register it
315 |
316 |
317 |
318 | ### open_config/3 ###
319 |
320 |
321 | open_config(ConfigName::config_name(), IniFiles::inifiles(), Options::config_options()) -> ok | {error, any()}
322 |
323 |
324 |
325 | open or create an ini file an register it. See the
326 | register_config function for a list of available functions.
327 |
328 |
329 |
330 | ### prefix/2 ###
331 |
332 |
333 | prefix(ConfigName::config_name(), Prefix::string()) -> [{section(), [{string(), string()}]}]
334 |
335 |
336 |
337 | get all sections starting by Prefix
338 |
339 |
340 |
341 | ### register_config/2 ###
342 |
343 |
344 | register_config(ConfigName::config_name(), IniFiles::inifiles()) -> ok | {error, any()}
345 |
346 |
347 |
348 | register inifiles or config dirs
349 |
350 |
351 |
352 | ### register_config/3 ###
353 |
354 |
355 | register_config(ConfigName::config_name(), IniFiles::inifiles(), Options::config_options()) -> ok | {error, any()}
356 |
357 |
358 |
359 | register inifiles of config dirs with options
360 | For now the only option is`autoreload` to auto reload the config on
361 | files or dirs changes.
362 | Configs can also be registererd in the app configuration at startup:
363 |
364 | [confs, [{ConfigName, IniFile},
365 | {ConfigName1, IniFiles1, [Options]}, ..]]
366 |
367 | Options:
368 |
369 | - `autoreload`: auto reload the config on files or dirs changes
370 | - `{autoreload, Delay}`: autoreload the config file or dir
371 | changes. Delay set the time between each scan. Default is 5000
372 | and can be set using the `scan_delay` application environement
373 | for econfig.
374 |
375 |
376 |
377 | ### reload/1 ###
378 |
379 |
380 | reload(ConfigName::config_name()) -> ok
381 |
382 |
383 |
384 | reload the configuration
385 |
386 |
387 |
388 | ### reload/2 ###
389 |
390 |
391 | reload(ConfigName::config_name(), IniFiles::inifiles()) -> ok
392 |
393 |
394 |
395 | reload the configuration
396 |
397 |
398 |
399 | ### sections/1 ###
400 |
401 |
402 | sections(ConfigName::config_name()) -> [section()]
403 |
404 |
405 |
406 | get all sections of a configuration
407 |
408 |
409 |
410 | ### set_value/3 ###
411 |
412 |
413 | set_value(ConfigName::config_name(), Section::section(), KVs::kvs()) -> ok
414 |
415 |
416 |
417 | set a list of key/value for a section
418 |
419 |
420 |
421 | ### set_value/4 ###
422 |
423 |
424 | set_value(ConfigName::config_name(), Section::section(), Key::any(), Value::any()) -> ok
425 |
426 |
427 |
428 | set a value and persist it to the file
429 |
430 |
431 |
432 | ### set_value/5 ###
433 |
434 |
435 | set_value(ConfigName::config_name(), Section::section(), Key::any(), Value::any(), Persist::boolean()) -> ok
436 |
437 |
438 |
439 | set a value and optionnaly persist it.
440 |
441 |
442 |
443 | ### start_autoreload/1 ###
444 |
445 |
446 | start_autoreload(ConfigName::config_name()) -> ok
447 |
448 |
449 |
450 | start the config watcher.
451 |
452 |
453 |
454 | ### stop_autoreload/1 ###
455 |
456 |
457 | stop_autoreload(ConfigName::config_name()) -> ok
458 |
459 |
460 |
461 | stop the config watcher.
462 |
463 |
464 |
465 | ### subscribe/1 ###
466 |
467 |
468 | subscribe(ConfigName::config_name()) -> ok
469 |
470 |
471 |
472 | Subscribe to config events for a config named `ConfigName`
473 |
474 | The message received to each subscriber will be of the form:
475 |
476 | - `{config_updated, ConfigName, reload}`
477 | - `{config_updated, ConfigName, registered}`
478 | - `{config_updated, ConfigName, unregistered}`
479 | - `{config_updated, ConfigName, {set, {Section, Key}}`
480 | - `{config_updated, ConfigName, {delete, {Section, Key}}`
481 |
482 |
483 |
484 | ### unregister_config/1 ###
485 |
486 |
487 | unregister_config(ConfigName::config_name()) -> ok
488 |
489 |
490 |
491 | unregister a conf
492 |
493 |
494 |
495 | ### unsubscribe/1 ###
496 |
497 |
498 | unsubscribe(ConfigName::config_name()) -> ok
499 |
500 |
501 |
502 | Remove all subscribtions to ConfigName events for this process
503 |
504 |
--------------------------------------------------------------------------------
/doc/edoc-info:
--------------------------------------------------------------------------------
1 | %% encoding: UTF-8
2 | {application,econfig}.
3 | {modules,[econfig]}.
4 |
--------------------------------------------------------------------------------
/doc/erlang.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoitc/econfig/84d7cec15f321c4ac5f5c6e08e5a9a8adea01986/doc/erlang.png
--------------------------------------------------------------------------------
/doc/overview.edoc:
--------------------------------------------------------------------------------
1 | %%==============================================================================
2 | %% Copyright 2012-2013 Benoît Chesneau
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 | %%==============================================================================
16 |
17 | @copyright 2012-2016 Benoît Chesneau.
18 | @version 0.7.3
19 | @title econfig - simple Erlang config handler using INI files
20 |
21 | @doc # econfig
22 |
23 | econfig is a simple Erlang config handler to manage a config from INI
24 | files.
25 |
26 | [](https://travis-ci.org/benoitc/econfig)
27 | [](https://hex.pm/packages/econfig)
28 |
29 |
30 | econfig can be use to read and update INI files. Values are cached in an
31 | ETS table and you can manage multiple configuration profiles. A process
32 | can also subscribe to config updates events.
33 |
34 | Autoreload of the config when an INI file is updated is supported, you can even
35 | manage changes from a full config directory.
36 |
37 | See the [NEWS](http://github.com/benoitc/econfig/blob/master/NEWS.md)
38 | for last changes.
39 |
40 | #### Useful modules are:
41 |
42 | - {@link econfig}: main module. It contains all the API.
43 |
44 | ## Examples
45 |
46 | Quick usage example:
47 |
48 | ```
49 | 1> application:ensure_all_started(econfig).
50 | ok
51 | 2> econfig:register_config(test, ["test/fixtures/test.ini", "test/fixtures/test2.ini"], [autoreload]).
52 | ok
53 | 3> econfig:subscribe(test).
54 | true
55 | 4> econfig:get_value(test, "section1").
56 | [{"key 3","value 3"},
57 | {"key1","value1"},
58 | {"key2","value 2"},
59 | {"key4","value 4"},
60 | {"key5","value5"}]
61 | 5> econfig:set_value(test, "section1", "key6", "value6").
62 | ok
63 | 6> flush().
64 | Shell got {config_updated,test,{set,{"section1","key6"}}}
65 | ok
66 | '''
67 |
68 | ## Advanced features
69 |
70 | ### on_change hook
71 |
72 | Some application may want to handle changes without suscribing to change. This change allows a user to pass a change function when registering the configuation. This function will be called each time a change happen.
73 |
74 | ### helpers functions
75 |
76 | econfig do not guess datatypes of values in configuration files, always storing them internally as strings. This means that if you need other datatypes, you should convert on your own. Some helpers are provided to do it:
77 |
78 | - `econfig:get_boolean/{3, 4}': to convert to boolean
79 | - `econfig:get_integer/{3, 4}': to convert to integer
80 | - `econfig:get_float/{3, 4}': to convert to float
81 | - `econfig:get_list/{3, 4}': to convert a list of string separated by `,' to a list.
82 | - `econfig:get_binary/{3, 4}': to convert to a binary
83 |
84 | Contribute
85 | ----------
86 | For issues, comments or feedback please [create an issue!] [1]
87 |
88 | [1]: http://github.com/benoitc/econfig/issues "econfig issues"
89 |
90 | @end
91 |
--------------------------------------------------------------------------------
/doc/stylesheet.css:
--------------------------------------------------------------------------------
1 | /* standard EDoc style sheet */
2 | body {
3 | font-family: Verdana, Arial, Helvetica, sans-serif;
4 | margin-left: .25in;
5 | margin-right: .2in;
6 | margin-top: 0.2in;
7 | margin-bottom: 0.2in;
8 | color: #000000;
9 | background-color: #ffffff;
10 | }
11 | h1,h2 {
12 | margin-left: -0.2in;
13 | }
14 | div.navbar {
15 | background-color: #add8e6;
16 | padding: 0.2em;
17 | }
18 | h2.indextitle {
19 | padding: 0.4em;
20 | background-color: #add8e6;
21 | }
22 | h3.function,h3.typedecl {
23 | background-color: #add8e6;
24 | padding-left: 1em;
25 | }
26 | div.spec {
27 | margin-left: 2em;
28 | background-color: #eeeeee;
29 | }
30 | a.module {
31 | text-decoration:none
32 | }
33 | a.module:hover {
34 | background-color: #eeeeee;
35 | }
36 | ul.definitions {
37 | list-style-type: none;
38 | }
39 | ul.index {
40 | list-style-type: none;
41 | background-color: #eeeeee;
42 | }
43 |
44 | /*
45 | * Minor style tweaks
46 | */
47 | ul {
48 | list-style-type: square;
49 | }
50 | table {
51 | border-collapse: collapse;
52 | }
53 | td {
54 | padding: 3
55 | }
56 |
--------------------------------------------------------------------------------
/rebar.config:
--------------------------------------------------------------------------------
1 | %% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*-
2 | %% ex: ft=erlang ts=4 sw=4 et
3 |
4 | {erl_opts, [debug_info]}.
5 | {xref_checks, [undefined_function_calls]}.
6 |
7 | {cover_enabled, true}.
8 | {eunit_opts, [verbose]}.
9 |
10 | {deps, []}.
11 |
12 | {profiles, [{docs, [
13 | {deps, [
14 | {edown,
15 | {git, "https://github.com/uwiger/edown.git",
16 | {tag, "0.7"}}}
17 | ]},
18 |
19 | {edoc_opts, [{doclet, edown_doclet},
20 | {app_default, "http://www.erlang.org/doc/man"},
21 | {top_level_readme,
22 | {"./README.md", "http://github.com/benoitc/econfig"}}]}
23 | ]}]}.
--------------------------------------------------------------------------------
/rebar.lock:
--------------------------------------------------------------------------------
1 | [].
2 |
--------------------------------------------------------------------------------
/src/econfig.app.src:
--------------------------------------------------------------------------------
1 | %% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*-
2 | %% ex: ft=erlang ts=4 sw=4 et
3 |
4 | {application, econfig,
5 | [
6 | {description, "simple Erlang config handler using INI files"},
7 | {vsn, "0.7.3"},
8 | {registered, [econfig_sup]},
9 | {applications, [kernel,
10 | stdlib]},
11 | {mod, { econfig_app, []}},
12 | {maintainers, ["Benoit Chesneau"]},
13 | {licenses, ["Apache License 2"]},
14 | {links, [{"Github", "https://github.com/benoitc/econfig"}]},
15 |
16 | {requirements, [{}]}
17 | ]}.
18 |
--------------------------------------------------------------------------------
/src/econfig.erl:
--------------------------------------------------------------------------------
1 | %%% -*- erlang -*-
2 | %%%
3 | %%% This file is part of econfig released under the Apache 2 license.
4 | %%% See the NOTICE for more information.
5 |
6 | %% @doc Public API of econfig
7 | %% econfig rely on a central gen_server an an ETS ordered-set.
8 | %% @end
9 | -module(econfig).
10 |
11 | -export([register_config/2, register_config/3,
12 | open_config/2, open_config/3,
13 | unregister_config/1,
14 | subscribe/1, unsubscribe/1,
15 | reload/1, reload/2,
16 | start_autoreload/1, stop_autoreload/1,
17 | all/1, sections/1, prefix/2,
18 | cfg2list/1, cfg2list/2,
19 | get_value/2, get_value/3, get_value/4,
20 | set_value/3, set_value/4, set_value/5,
21 | delete_value/2, delete_value/3, delete_value/4]).
22 |
23 | %% helper functions
24 | -export([get_boolean/3, get_boolean/4,
25 | get_integer/3, get_integer/4,
26 | get_float/3, get_float/4,
27 | get_list/3, get_list/4,
28 | get_binary/3, get_binary/4]).
29 |
30 | -type config_name() :: atom() | string() | binary().
31 | -type inifile() :: string().
32 | -type inifiles() :: [inifile()].
33 | -type config_options() :: [autoreload | {autoreload, integer}
34 | | {change_fun, fun()}].
35 | -type section() :: string().
36 | -type kvs() :: [{any(), any()}].
37 |
38 | -export_type([config_name/0, inifile/0, inifiles/0, config_options/0, section/0, kvs/0]).
39 |
40 | %% @doc register inifiles or config dirs
41 | -spec register_config(ConfigName::config_name(), IniFiles::inifiles()) -> ok | {error, any()}.
42 | register_config(ConfigName, IniFiles) ->
43 | econfig_server:register_config(ConfigName, IniFiles).
44 |
45 |
46 | %% @doc register inifiles of config dirs with options
47 | %% For now the only option is `autoreload' to auto reload the config on
48 | %% files or dirs changes.
49 | %% Configs can also be registererd in the app configuration at startup:
50 | %%
51 | %% [confs, [{ConfigName, IniFile},
52 | %% {ConfigName1, IniFiles1, [Options]}, ..]]
53 | %%
54 | %% Options:
55 | %%
56 | %% - `autoreload': auto reload the config on files or dirs changes
57 | %% - `{autoreload, Delay}': autoreload the config file or dir
58 | %% changes. Delay set the time between each scan. Default is 5000
59 | %% and can be set using the `scan_delay' application environement
60 | %% for econfig.
61 | -spec register_config(ConfigName::config_name(), IniFiles::inifiles(), Options::config_options()) -> ok | {error, any()}.
62 | register_config(ConfigName, IniFiles, Options) ->
63 | econfig_server:register_config(ConfigName, IniFiles, Options).
64 |
65 |
66 | %% @doc unregister a conf
67 | -spec unregister_config(config_name()) -> ok.
68 | unregister_config(ConfigName) ->
69 | econfig_server:unregister_config(ConfigName).
70 |
71 | %% @doc open or create an ini file an register it
72 | -spec open_config(ConfigName::config_name(), IniFiles::inifiles()) -> ok | {error, any()}.
73 | open_config(ConfigName, IniFile) ->
74 | econfig_server:open_config(ConfigName, IniFile).
75 |
76 | %% @doc open or create an ini file an register it. See the
77 | %% register_config function for a list of available functions.
78 | -spec open_config(ConfigName::config_name(), IniFiles::inifiles(), Options::config_options()) -> ok | {error, any()}.
79 | open_config(ConfigName, IniFile, Options) ->
80 | econfig_server:open_config(ConfigName, IniFile, Options).
81 |
82 | %% @doc Subscribe to config events for a config named `ConfigName'
83 | %%
84 | %% The message received to each subscriber will be of the form:
85 | %%
86 | %%
87 | %% - `{config_updated, ConfigName, reload}'
88 | %% - `{config_updated, ConfigName, registered}'
89 | %% - `{config_updated, ConfigName, unregistered}'
90 | %% - `{config_updated, ConfigName, {set, {Section, Key}}'
91 | %% - `{config_updated, ConfigName, {delete, {Section, Key}}'
92 | %%
93 | %% @end
94 | -spec subscribe(ConfigName::config_name()) -> ok.
95 | subscribe(ConfigName) ->
96 | econfig_server:subscribe(ConfigName).
97 |
98 | %% @doc Remove all subscribtions to ConfigName events for this process
99 | %%
100 | %% @end
101 | -spec unsubscribe(ConfigName::config_name()) -> ok.
102 | unsubscribe(ConfigName) ->
103 | econfig_server:unsubscribe(ConfigName).
104 |
105 | %% @doc reload the configuration
106 | -spec reload(ConfigName::config_name()) -> ok.
107 | reload(ConfigName) ->
108 | econfig_server:reload(ConfigName).
109 |
110 | %% @doc reload the configuration
111 | -spec reload(ConfigName::config_name(), IniFiles::inifiles()) -> ok.
112 | reload(ConfigName, IniFiles) ->
113 | econfig_server:reload(ConfigName, IniFiles).
114 |
115 | %% @doc start the config watcher.
116 | -spec start_autoreload(ConfigName::config_name()) -> ok.
117 | start_autoreload(ConfigName) ->
118 | econfig_server:start_autoreload(ConfigName).
119 |
120 | %% @doc stop the config watcher.
121 | -spec stop_autoreload(ConfigName::config_name()) -> ok.
122 | stop_autoreload(ConfigName) ->
123 | econfig_server:stop_autoreload(ConfigName).
124 |
125 | %% @doc get all values of a configuration
126 | -spec all(ConfigName::config_name()) -> [{section(), [{string(), string()}]}].
127 | all(ConfigName) ->
128 | econfig_server:all(ConfigName).
129 |
130 | %% @doc get all sections of a configuration
131 | -spec sections(ConfigName::config_name()) -> [section()].
132 | sections(ConfigName) ->
133 | econfig_server:sections(ConfigName).
134 |
135 | %% @doc get all sections starting by Prefix
136 | -spec prefix(ConfigName::config_name(), Prefix::string()) -> [{section(), [{string(), string()}]}].
137 | prefix(ConfigName, Prefix) ->
138 | econfig_server:prefix(ConfigName, Prefix).
139 |
140 | %% @doc retrive config as a proplist
141 | -spec cfg2list(ConfigName::config_name()) -> [{section(), [{string(), string()}]}].
142 | cfg2list(ConfigName) ->
143 | econfig_server:cfg2list(ConfigName).
144 |
145 | %% @doc retrieve config as a proplist
146 | -spec cfg2list(ConfigName::config_name(), GroupKey::string()) -> [{section(), [{string(), string()}]}].
147 | cfg2list(ConfigName, GroupKey) ->
148 | econfig_server:cfg2list(ConfigName, GroupKey).
149 |
150 | %% @doc get keys/values of a section
151 | -spec get_value(ConfigName::config_name(), Section::string()) -> [{string(), string()}].
152 | get_value(ConfigName, Section) ->
153 | econfig_server:get_value(ConfigName, Section).
154 |
155 | %% @doc get value for a key in a section
156 | -spec get_value(ConfigName::config_name(), Section::section(), Key::any()) -> Value::string() | undefined.
157 | get_value(ConfigName, Section, Key) ->
158 | econfig_server:get_value(ConfigName, Section, Key).
159 |
160 | %% @doc get value for a key in a section or return the default value if not set
161 | -spec get_value(ConfigName::config_name(), Section::section(), Key::any(), Default::any()) -> Value::string().
162 | get_value(ConfigName, Section, Key, Default) ->
163 | econfig_server:get_value(ConfigName, Section, Key, Default).
164 |
165 | %% @doc set a list of key/value for a section
166 | -spec set_value(ConfigName::config_name(), Section::section(), KVs::kvs()) -> ok.
167 | set_value(ConfigName, Section, KVs) ->
168 | econfig_server:set_value(ConfigName, Section, KVs).
169 |
170 | %% @doc set a value and persist it to the file
171 | -spec set_value(ConfigName::config_name(), Section::section(), Key::any(), Value::any()) -> ok.
172 | set_value(ConfigName, Section, Key, Value) ->
173 | set_value(ConfigName, Section, Key, Value, true).
174 |
175 | %% @doc set a value and optionnaly persist it.
176 | -spec set_value(ConfigName::config_name(), Section::section(), Key::any(), Value::any(), Persist::boolean()) -> ok.
177 | set_value(ConfigName, Section, Key, Value, Persist) ->
178 | econfig_server:set_value(ConfigName, Section, Key, Value, Persist).
179 |
180 | %% @doc delete all key/values from a section
181 | -spec delete_value(ConfigName::config_name(), Section::section()) -> ok.
182 | delete_value(ConfigName, Section) ->
183 | econfig_server:delete_value(ConfigName, Section).
184 |
185 | %% @doc delete a value and persist the change to the file
186 | -spec delete_value(ConfigName::config_name(), Section::section(), Key::any()) -> ok.
187 | delete_value(ConfigName, Section, Key) ->
188 | econfig_server:delete_value(ConfigName, Section, Key).
189 |
190 | %% @doc delete a value and optionnally persist it
191 | -spec delete_value(ConfigName::config_name(), Section::section(), Key::any(), Persist::boolean()) -> ok.
192 | delete_value(ConfigName, Section, Key, Persist) ->
193 | econfig_server:delete_value(ConfigName, Section, Key, Persist).
194 |
195 | %% @doc get a value and convert it to a boolean if possible
196 | %% This method is case-insensitive and recognizes Boolean values from 'yes'/'no', 'on'/'off', 'true'/'false' and '1'/'0'
197 | %% a badarg error is raised if the value can't be parsed to a boolean
198 | -spec get_boolean(ConfigName::config_name(), Section::section(), Key::any()) -> Value::boolean() | undefined.
199 | get_boolean(ConfigName, Section, Key) ->
200 | case get_value(ConfigName, Section, Key) of
201 | undefined -> undefined;
202 | Val -> to_boolean(Val)
203 | end.
204 |
205 | %% @doc get a value and convert it to a boolean if possible. It fallback to default if not set.
206 | %% This method is case-insensitive and recognizes Boolean values from 'yes'/'no', 'on'/'off', 'true'/'false' and '1'/'0'
207 | %% a badarg error is raised if the value can't be parsed to a boolean
208 | -spec get_boolean(ConfigName::config_name(), Section::section(), Key::any(), Default::boolean()) -> Value::boolean().
209 | get_boolean(ConfigName, Section, Key, Default) when is_boolean(Default) ->
210 | case get_boolean(ConfigName, Section, Key) of
211 | undefined -> Default;
212 | Val -> Val
213 | end.
214 |
215 | %% @doc get a value and convert it to an integer
216 | -spec get_integer(ConfigName::config_name(), Section::section(), Key::any()) -> Value::integer() | undefined.
217 | get_integer(ConfigName, Section, Key) ->
218 | case get_value(ConfigName, Section, Key) of
219 | undefined -> undefined;
220 | Val -> to_int(Val)
221 | end.
222 |
223 | %% @doc get a value and convert it to an integer
224 | -spec get_integer(ConfigName::config_name(), Section::section(), Key::any(), Default::integer()) -> Value::integer().
225 | get_integer(ConfigName, Section, Key, Default) when is_integer(Default) ->
226 | case get_integer(ConfigName, Section, Key) of
227 | undefined -> Default;
228 | IVal -> IVal
229 | end.
230 |
231 | %% @doc get a value and convert it to an float
232 | -spec get_float(ConfigName::config_name(), Section::section(), Key::any()) -> Value::float() | undefined.
233 | get_float(ConfigName, Section, Key) ->
234 | case get_value(ConfigName, Section, Key) of
235 | undefined -> undefined;
236 | Val -> to_float(Val)
237 | end.
238 |
239 | %% @doc get a value and convert it to an float
240 | -spec get_float(ConfigName::config_name(), Section::section(), Key::any(), Default::float()) -> Value::float().
241 | get_float(ConfigName, Section, Key, Default) when is_float(Default) ->
242 | case get_float(ConfigName, Section, Key) of
243 | undefined -> Default;
244 | IVal -> IVal
245 | end.
246 |
247 | %% @doc get a value and convert it to an list
248 | -spec get_list(ConfigName::config_name(), Section::section(), Key::any()) -> Value::list() | undefined.
249 | get_list(ConfigName, Section, Key) ->
250 | case get_value(ConfigName, Section, Key) of
251 | undefined -> undefined;
252 | Val -> to_list(Val)
253 | end.
254 |
255 | %% @doc get a value and convert it to an list
256 | -spec get_list(ConfigName::config_name(), Section::section(), Key::any(), Default::list()) -> Value::list().
257 | get_list(ConfigName, Section, Key, Default) when is_list(Default) ->
258 | case get_list(ConfigName, Section, Key) of
259 | undefined -> Default;
260 | LVal -> LVal
261 | end.
262 |
263 | %% @doc get a value and convert it to an binary
264 | -spec get_binary(ConfigName::config_name(), Section::section(), Key::any()) -> Value::binary() | undefined.
265 | get_binary(ConfigName, Section, Key) ->
266 | case get_value(ConfigName, Section, Key) of
267 | undefined -> undefined;
268 | Val -> to_binary(Val)
269 | end.
270 |
271 | %% @doc get a value and convert it to an binary
272 | -spec get_binary(ConfigName::config_name(), Section::section(), Key::any(), Default::binary()) -> Value::binary().
273 | get_binary(ConfigName, Section, Key, Default) when is_binary(Default) ->
274 | case get_binary(ConfigName, Section, Key) of
275 | undefined -> Default;
276 | Val -> Val
277 | end.
278 |
279 |
280 | to_boolean(Val) ->
281 | case string:to_lower(Val) of
282 | "true" -> true;
283 | "false" -> false;
284 | "1" -> true;
285 | "0" -> false;
286 | "on" -> true;
287 | "off" -> false;
288 | "yes" -> true;
289 | "no" -> false;
290 | undefined -> undefined;
291 | _ ->
292 | error(badarg)
293 | end.
294 |
295 | to_int(Val) ->
296 | case string:to_integer(Val) of
297 | {IVal, []} -> IVal;
298 | _ -> error(badarg)
299 | end.
300 |
301 | to_float(Val) ->
302 | case string:to_float(Val) of
303 | {FVal, []} -> FVal;
304 | _ -> error(badarg)
305 | end.
306 |
307 | to_list("") -> [];
308 | to_list(Val) ->
309 | lists:filtermap(fun(V) ->
310 | case string:strip(V) of
311 | "" -> false;
312 | V2 -> {true, V2}
313 | end
314 | end, string:tokens(Val, ",")).
315 |
316 | to_binary(Val) ->
317 | try list_to_binary(Val) of
318 | Bin -> Bin
319 | catch
320 | _ -> error(badarg)
321 | end.
322 |
323 |
324 | -ifdef(TEST).
325 |
326 | -include_lib("eunit/include/eunit.hrl").
327 |
328 | -compile(export_all).
329 |
330 | fixture_path(Name) ->
331 | EbinDir = filename:dirname(code:which(?MODULE)),
332 | AppPath = filename:dirname(EbinDir),
333 | filename:join([AppPath, "test", "fixtures", Name]).
334 |
335 |
336 |
337 | setup_common() ->
338 | {ok, _} = application:ensure_all_started(econfig),
339 | ok = file:write_file(fixture_path("local.ini"), <<>>).
340 |
341 | setup() ->
342 | setup_common(),
343 | ok = econfig:register_config(t, [fixture_path("test.ini"), fixture_path("local.ini")], []).
344 |
345 | setup_multi() ->
346 | setup_common(),
347 | ok = econfig:register_config(t, [fixture_path("test.ini"), fixture_path("test2.ini"), fixture_path("local.ini")], []).
348 |
349 | cleanup(_State) ->
350 | error_logger:tty(false),
351 | ok = application:stop(econfig),
352 | error_logger:tty(true),
353 | ok = file:delete(fixture_path("local.ini")).
354 |
355 | config_change(Change) ->
356 | ets:insert(econfig_test, Change).
357 |
358 | setup_change_fun() ->
359 | setup_common(),
360 | ets:new(econfig_test, [named_table, public]),
361 | ChangeFun = fun(Change) ->
362 | ets:insert(econfig_test, Change)
363 | end,
364 | ok = econfig:register_config(t, [fixture_path("test.ini"), fixture_path("local.ini")], [{change_fun, ChangeFun}]).
365 |
366 | setup_change_fun2() ->
367 | setup_common(),
368 | ets:new(econfig_test, [named_table, public]),
369 | ok = econfig:register_config(t, [fixture_path("test.ini"), fixture_path("local.ini")], [{change_fun, {?MODULE, config_change}}]).
370 |
371 | cleanup_change_fun(State) ->
372 | ets:delete(econfig_test),
373 | cleanup(State).
374 |
375 | parse_test_() ->
376 | {setup,
377 | fun setup/0,
378 | fun cleanup/1,
379 | [?_assertEqual(lists:sort(["section1", "section 2", "section3", "section4"]), lists:sort(econfig:sections(t))),
380 | ?_assertEqual("value1", econfig:get_value(t, "section1", "key1")),
381 | ?_assertEqual("value 2", econfig:get_value(t, "section1", "key2")),
382 | ?_assertEqual("value 3", econfig:get_value(t, "section1", "key 3")),
383 | ?_assertEqual("value 4", econfig:get_value(t, "section1", "key4")),
384 | ?_assertEqual("value5", econfig:get_value(t, "section1", "key5")),
385 | ?_assertEqual("value6", econfig:get_value(t, "section 2", "key6")),
386 | ?_assertEqual("value7", econfig:get_value(t, "section 2", "key7")),
387 | ?_assertEqual("value8", econfig:get_value(t, "section 2", "key8")),
388 | ?_assertEqual("value 9", econfig:get_value(t, "section 2", "key9")),
389 | ?_assertEqual("value10", econfig:get_value(t, "section 2", "key10")),
390 | ?_assertEqual("new-val-11", econfig:get_value(t, "section3", "key11")),
391 | ?_assertEqual("this is a value for key 13", econfig:get_value(t, "section3", "key13")),
392 | ?_assertEqual("some-collection.of+random@characters", econfig:get_value(t, "section3", "key14")),
393 | ?_assertEqual(undefined, econfig:get_value(t, "section3", "key15"))
394 | ]}.
395 |
396 |
397 | parse_with_helpers_test_() ->
398 | {setup,
399 | fun setup/0,
400 | fun cleanup/1,
401 | [?_assertEqual(1, econfig:get_integer(t, "section4", "key1")),
402 | ?_assertEqual(undefined, econfig:get_integer(t, "section4", "key11")),
403 | ?_assertMatch({'EXIT',{badarg, _}}, (catch econfig:get_integer(t, "section4", "key2"))),
404 | ?_assertEqual(11, econfig:get_integer(t, "section4", "key11", 11)),
405 | ?_assertEqual(true, econfig:get_boolean(t, "section4", "key2")),
406 | ?_assertEqual(false, econfig:get_boolean(t, "section4", "key3")),
407 | ?_assertEqual(false, econfig:get_boolean(t, "section4", "key33", false)),
408 | ?_assertMatch({'EXIT',{badarg, _}}, (catch econfig:get_boolean(t, "section4", "key4"))),
409 | ?_assertEqual(["a", "b"], econfig:get_list(t, "section4", "key4")),
410 | ?_assertEqual(["c", "d"], econfig:get_list(t, "section4", "key44", ["c", "d"])),
411 | ?_assertEqual(1.4, econfig:get_float(t, "section4", "key5")),
412 | ?_assertEqual(<<"test">>, econfig:get_binary(t, "section4", "key6"))
413 | ]}.
414 |
415 | parse_multi_test_() ->
416 | {setup,
417 | fun setup_multi/0,
418 | fun cleanup/1,
419 | [% matching section/key should have value from latter file
420 | ?_assertEqual("value6 overwrite", econfig:get_value(t, "section 2", "key6")),
421 | % non-matching from first file
422 | ?_assertEqual("value10", econfig:get_value(t, "section 2", "key10")),
423 | % non-matching from last file
424 | ?_assertEqual("value 888", econfig:get_value(t, "section 2", "key888"))
425 | ]}.
426 |
427 | modify_test_() ->
428 | {setup,
429 | fun setup/0,
430 | fun cleanup/1,
431 | {inorder,
432 | [% modify existing key
433 | fun() ->
434 | econfig:set_value(t, "section 2", "key6", "value6_modified"),
435 | ?assertEqual("value6_modified", econfig:get_value(t, "section 2", "key6"))
436 | end,
437 | % delete an existing key
438 | fun() ->
439 | econfig:delete_value(t, "section 2", "key6"),
440 | ?assertEqual(undefined, econfig:get_value(t, "section 2", "key6"))
441 | end,
442 | % delete an existing key and reload
443 | fun() ->
444 | econfig:delete_value(t, "section 2", "key6"),
445 | econfig:reload(t),
446 | ?assertEqual(undefined, econfig:get_value(t, "section 2", "key6"))
447 | end,
448 | % add a new key
449 | fun() ->
450 | econfig:set_value(t, "section 2", "key666", "value666"),
451 | ?assertEqual("value666", econfig:get_value(t, "section 2", "key666"))
452 | end,
453 | % modify to empty
454 | fun() ->
455 | econfig:set_value(t, "section 2", "key7", ""),
456 | ?assertEqual(undefined, econfig:get_value(t, "section 2", "key7"))
457 | end,
458 | % modify to empty and reload
459 | fun() ->
460 | econfig:set_value(t, "section 2", "key7", ""),
461 | econfig:reload(t),
462 | ?assertEqual(undefined, econfig:get_value(t, "section 2", "key7"))
463 | end,
464 |
465 | % store integer and retrieved
466 | fun() ->
467 | econfig:set_value(t, "section 2", "key7", 10),
468 | econfig:reload(t),
469 | ?assertEqual(10, econfig:get_integer(t, "section 2", "key7"))
470 | end,
471 |
472 | % store boolean and retrieved
473 | fun() ->
474 | econfig:set_value(t, "section 2", "key7", true),
475 | econfig:reload(t),
476 | ?assertEqual(true, econfig:get_boolean(t, "section 2", "key7"))
477 | end,
478 |
479 | % store float and retrieved
480 | fun() ->
481 | econfig:set_value(t, "section 2", "key7", 1.4),
482 | econfig:reload(t),
483 | ?assertEqual(1.4, econfig:get_float(t, "section 2", "key7"))
484 | end
485 | ]}}.
486 |
487 | modify_multi_test_() ->
488 | {setup,
489 | fun setup_multi/0,
490 | fun cleanup/1,
491 | {inorder,
492 | [% modify existing key first file
493 | fun() ->
494 | econfig:set_value(t, "section 2", "key10", "value10modified"),
495 | ?assertEqual("value10modified", econfig:get_value(t, "section 2", "key10"))
496 | end,
497 | % modify existing key first file with reload
498 | fun() ->
499 | econfig:set_value(t, "section 2", "key10", "value10modified"),
500 | econfig:reload(t),
501 | ?assertEqual("value10modified", econfig:get_value(t, "section 2", "key10"))
502 | end,
503 | % modify existing key last file
504 | fun() ->
505 | econfig:set_value(t, "section 2", "key888", "value 888 modified"),
506 | ?assertEqual("value 888 modified", econfig:get_value(t, "section 2", "key888"))
507 | end,
508 | % modify existing key last file with reload
509 | fun() ->
510 | econfig:set_value(t, "section 2", "key888", "value 888 modified"),
511 | econfig:reload(t),
512 | ?assertEqual("value 888 modified", econfig:get_value(t, "section 2", "key888"))
513 | end,
514 | % modify existing matching key
515 | fun() ->
516 | econfig:set_value(t, "section 2", "key6", "value6modified"),
517 | ?assertEqual("value6modified", econfig:get_value(t, "section 2", "key6"))
518 | end,
519 | % modify existing matching key with reload
520 | fun() ->
521 | econfig:set_value(t, "section 2", "key6", "value6modified"),
522 | econfig:reload(t),
523 | ?assertEqual("value6modified", econfig:get_value(t, "section 2", "key6"))
524 | end,
525 | % modify existing matching key to empty value
526 | fun() ->
527 | econfig:set_value(t, "section 2", "key6", ""),
528 | ?assertEqual(undefined, econfig:get_value(t, "section 2", "key6"))
529 | end,
530 | % modify existing matching key to empty value with reload
531 | fun() ->
532 | econfig:set_value(t, "section 2", "key6", ""),
533 | econfig:reload(t),
534 | ?assertEqual(undefined, econfig:get_value(t, "section 2", "key6"))
535 | end,
536 | % delete existing matching key
537 | fun() ->
538 | econfig:delete_value(t, "section 2", "key7"),
539 | ?assertEqual(undefined, econfig:get_value(t, "section 2", "key7"))
540 | end,
541 | % modify existing key via list
542 | fun() ->
543 | econfig:set_value(t, "section10", [{"key1", "value1"}, {"key2", ""}]),
544 | ?assertEqual("value1", econfig:get_value(t, "section10", "key1"))
545 | end,
546 | % modify existing key to empty value via list
547 | fun() ->
548 | econfig:set_value(t, "section10", [{"key1", "value1"}, {"key2", ""}]),
549 | ?assertEqual(undefined, econfig:get_value(t, "section10", "key2"))
550 | end,
551 | % modify existing key to empty value via list with reload
552 | fun() ->
553 | econfig:set_value(t, "section10", [{"key1", "value1"}, {"key2", ""}]),
554 | econfig:reload(t),
555 | ?assertEqual(undefined, econfig:get_value(t, "section10", "key2"))
556 | end
557 | ]}}.
558 |
559 | subscribe_test_() ->
560 | {setup,
561 | fun setup/0,
562 | fun cleanup/1,
563 | [% test basic subscribe/unsubscribe results
564 | fun() ->
565 | ResultSubscribe = econfig:subscribe(t),
566 | ResultUnsubscribe = econfig:unsubscribe(t),
567 | ?assertEqual([ok, ok], [ResultSubscribe, ResultUnsubscribe])
568 | end,
569 |
570 | % test subscribe update
571 | fun() ->
572 | econfig:subscribe(t),
573 | econfig:set_value(t, "section 2", "key666", "value666"),
574 | Messages = changes_loop([], 0, 1),
575 | econfig:unsubscribe(t),
576 |
577 | Expected = [
578 | {config_updated,t, {set, {"section 2", "key666"}}}
579 | ],
580 |
581 | ?assertEqual(Expected, Messages)
582 | end,
583 |
584 | % test subscribe delete
585 | fun() ->
586 | econfig:subscribe(t),
587 | econfig:delete_value(t, "section 2", "key6"),
588 | Messages = changes_loop([], 0, 2),
589 | econfig:unsubscribe(t),
590 |
591 | Expected = [
592 | {config_updated, t, {delete, {"section 2", "key6"}}}
593 | ],
594 |
595 | ?assertEqual(Expected, Messages)
596 | end,
597 |
598 | %% test multiple updates
599 | fun() ->
600 | econfig:subscribe(t),
601 | econfig:set_value(t, "section10", [{"key1", "value1"}, {"key2", ""}]),
602 | Messages = changes_loop([], 0, 2),
603 | econfig:unsubscribe(t),
604 |
605 | Expected = [
606 | {config_updated,t, {set, {"section10", "key1"}}},
607 | {config_updated, t, {delete, {"section10", "key2"}}}
608 | ],
609 |
610 | ?assertEqual(Expected, Messages)
611 | end,
612 |
613 | %% test multiple delete
614 | fun() ->
615 | econfig:set_value(t, "section10", [{"key1", "value1"}, {"key2", "value2"}]),
616 | econfig:subscribe(t),
617 | econfig:delete_value(t, "section10"),
618 | Messages = changes_loop([], 0, 2),
619 | econfig:unsubscribe(t),
620 |
621 | Expected = [
622 | {config_updated,t, {delete, {"section10", "key1"}}},
623 | {config_updated, t, {delete, {"section10", "key2"}}}
624 | ],
625 |
626 | ?assertEqual(Expected, Messages)
627 | end,
628 |
629 |
630 | % test unsubscribe
631 | fun() ->
632 | econfig:subscribe(t),
633 | econfig:unsubscribe(t),
634 | econfig:set_value(t, "section 2", "key666", "value666"),
635 | Result = receive
636 | _ ->
637 | true
638 | after 0 ->
639 | false
640 | end,
641 | ?assertEqual(false, Result)
642 | end
643 | ]}.
644 |
645 | change_fun_test_() ->
646 | {setup,
647 | fun setup_change_fun/0,
648 | fun cleanup_change_fun/1,
649 | [% test update
650 | fun() ->
651 | econfig:set_value(t, "section 2", "key666", "value666"),
652 | Changes = ets:tab2list(econfig_test),
653 | ?assertEqual([{config_updated, t, {set, {"section 2", "key666"}}}], Changes)
654 | end,
655 | % test subscribe delete
656 | fun() ->
657 | econfig:delete_value(t, "section 2", "key6"),
658 | Changes = ets:tab2list(econfig_test),
659 | ?assertEqual([{config_updated, t, {delete, {"section 2", "key6"}}}], Changes)
660 | end
661 | ]}.
662 |
663 | change_module_fun_test_() ->
664 | {setup,
665 | fun setup_change_fun2/0,
666 | fun cleanup_change_fun/1,
667 | [% test update
668 | fun() ->
669 | econfig:set_value(t, "section 2", "key666", "value666"),
670 | Changes = ets:tab2list(econfig_test),
671 | ?assertEqual([{config_updated, t, {set, {"section 2", "key666"}}}], Changes)
672 | end,
673 | % test subscribe delete
674 | fun() ->
675 | econfig:delete_value(t, "section 2", "key6"),
676 | Changes = ets:tab2list(econfig_test),
677 | ?assertEqual([{config_updated, t, {delete, {"section 2", "key6"}}}], Changes)
678 | end
679 | ]}.
680 |
681 | changes_loop(Acc, Max, Max) ->
682 | lists:reverse(Acc);
683 | changes_loop(Acc, I, Max) ->
684 | receive
685 | Msg -> changes_loop([Msg | Acc], I + 1, Max)
686 | after 500 ->
687 | changes_loop(Acc, I + 1, Max)
688 | end.
689 |
690 |
691 | -endif.
692 |
--------------------------------------------------------------------------------
/src/econfig_app.erl:
--------------------------------------------------------------------------------
1 | %%% -*- erlang -*-
2 | %%%
3 | %%% This file is part of econfig released under the Apache 2 license.
4 | %%% See the NOTICE for more information.
5 |
6 | %% @hidden
7 |
8 | -module(econfig_app).
9 |
10 | -behaviour(application).
11 |
12 | %% Application callbacks
13 | -export([start/2, stop/1]).
14 |
15 | %% ===================================================================
16 | %% Application callbacks
17 | %% ===================================================================
18 |
19 | start(_StartType, _StartArgs) ->
20 | econfig_sup:start_link().
21 |
22 | stop(_State) ->
23 | ok.
--------------------------------------------------------------------------------
/src/econfig_file_writer.erl:
--------------------------------------------------------------------------------
1 | %%% -*- erlang -*-
2 | %%%
3 | %%% This file is part of econfig released under the Apache 2 license.
4 | %%% See the NOTICE for more information.
5 |
6 | %% @hidden
7 |
8 | -module(econfig_file_writer).
9 |
10 | -export([save_to_file/2]).
11 |
12 | %% @spec save_to_file(
13 | %% Config::{{Section::string(), Option::string()}, Value::string()},
14 | %% File::filename()) -> ok
15 | %% @doc Saves a Section/Key/Value triple to the ini file File::filename()
16 | save_to_file({Section, KVs}, File) ->
17 | {ok, OldFileContents} = file:read_file(File),
18 | Lines0 = re:split(OldFileContents, "\r\n|\n|\r|\032", [{return, list}]),
19 |
20 | SectionLine = "[" ++ Section ++ "]",
21 |
22 | PKVs = lists:foldl(fun({Key, Val}, Acc) ->
23 | P0 = ["^(", Key, "\\s*=)|\\[[a-zA-Z0-9\.\_-]*\\]"],
24 | {ok, Pattern} = re:compile(P0),
25 | [{Pattern, {Key, Val}} | Acc]
26 | end, [], KVs),
27 |
28 | NewLines0 = lists:foldl(fun(PKV, Lines) ->
29 | Lines1 = process_file_lines(Lines, [], SectionLine,
30 | PKV),
31 | lists:reverse(Lines1)
32 | end, Lines0, PKVs),
33 |
34 | NewLines = lists:reverse(NewLines0),
35 |
36 | NewFileContents = reverse_and_add_newline(strip_empty_lines(NewLines), []),
37 | case file:write_file(File, NewFileContents) of
38 | ok ->
39 | ok;
40 | {error, eacces} ->
41 | {file_permission_error, File};
42 | Error ->
43 | Error
44 | end.
45 |
46 |
47 | process_file_lines([Section|Rest], SeenLines, Section, PKV) ->
48 | process_section_lines(Rest, [Section|SeenLines], PKV);
49 |
50 | process_file_lines([Line|Rest], SeenLines, Section, PKV) ->
51 | process_file_lines(Rest, [Line|SeenLines], Section, PKV);
52 |
53 | process_file_lines([], SeenLines, Section, {_, {Key, Value}}) ->
54 | % Section wasn't found. Append it with the option here.
55 |
56 | [Key ++ " = " ++ Value, Section, "" | strip_empty_lines(SeenLines)].
57 |
58 |
59 | process_section_lines([Line|Rest], SeenLines, {Pattern, {Key, Value}}=PKV) ->
60 | case re:run(Line, Pattern, [{capture, all_but_first}]) of
61 | nomatch -> % Found nothing interesting. Move on.
62 | process_section_lines(Rest, [Line|SeenLines], PKV);
63 | {match, []} -> % Found another section. Append the option here.
64 | lists:reverse(Rest) ++
65 | [Line, "", Key ++ " = " ++ Value | strip_empty_lines(SeenLines)];
66 | {match, _} -> % Found the option itself. Replace it.
67 | lists:reverse(Rest) ++ [Key ++ " = " ++ Value | SeenLines]
68 | end;
69 |
70 | process_section_lines([], SeenLines, {_Pattern, {Key, Value}}) ->
71 | % Found end of file within the section. Append the option here.
72 | [Key ++ " = " ++ Value | strip_empty_lines(SeenLines)].
73 |
74 |
75 | reverse_and_add_newline([Line|Rest], Content) ->
76 | reverse_and_add_newline(Rest, [Line, "\n", Content]);
77 |
78 | reverse_and_add_newline([], Content) ->
79 | Content.
80 |
81 |
82 | strip_empty_lines(["" | Rest]) ->
83 | strip_empty_lines(Rest);
84 |
85 | strip_empty_lines(All) ->
86 | All.
87 |
--------------------------------------------------------------------------------
/src/econfig_server.erl:
--------------------------------------------------------------------------------
1 | %%% -*- erlang -*-
2 | %%%
3 | %%% This file is part of econfig released under the Apache 2 license.
4 | %%% See the NOTICE for more information.
5 |
6 | %% @hidden
7 |
8 | -module(econfig_server).
9 | -behaviour(gen_server).
10 |
11 | -export([register_config/2, register_config/3,
12 | open_config/2, open_config/3,
13 | unregister_config/1,
14 | subscribe/1, unsubscribe/1,
15 | reload/1, reload/2,
16 | start_autoreload/1, stop_autoreload/1,
17 | all/1, sections/1, prefix/2,
18 | cfg2list/1, cfg2list/2,
19 | get_value/2, get_value/3, get_value/4,
20 | set_value/3, set_value/4, set_value/5,
21 | delete_value/2, delete_value/3, delete_value/4]).
22 |
23 | -export([start_link/0]).
24 |
25 | -export([init/1,
26 | handle_call/3,
27 | handle_cast/2,
28 | handle_info/2,
29 | terminate/2,
30 | code_change/3]).
31 |
32 | -record(state, {confs = dict:new()}).
33 | -record(config, {write_file=nil,
34 | pid=nil,
35 | change_fun,
36 | options,
37 | inifiles}).
38 |
39 | -define(TAB, econfig).
40 |
41 | start_link() ->
42 | _ = init_tabs(),
43 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
44 |
45 | -spec register_config(term(), econfig:inifiles()) -> ok | {error,
46 | any()}.
47 | %% @doc register inifiles
48 | register_config(ConfigName, IniFiles) ->
49 | register_config(ConfigName, IniFiles, []).
50 |
51 | register_config(ConfigName, IniFiles, Options) ->
52 | gen_server:call(?MODULE, {register_conf, {ConfigName, IniFiles, Options}}, infinity).
53 |
54 | %% @doc unregister a conf
55 | unregister_config(ConfigName) ->
56 | gen_server:call(?MODULE, {unregister_conf, ConfigName}).
57 |
58 | %% @doc open or create an ini file an register it
59 | open_config(ConfigName, IniFile) ->
60 | open_config(ConfigName, IniFile, []).
61 |
62 | open_config(ConfigName, IniFile, Options) ->
63 | IniFileName = econfig_util:abs_pathname(IniFile),
64 | case filelib:is_file(IniFileName) of
65 | true ->
66 | register_config(ConfigName, [IniFile], Options);
67 | false ->
68 | case file:open(IniFileName, [write]) of
69 | {ok, Fd} ->
70 | file:close(Fd),
71 | register_config(ConfigName, [IniFile], Options);
72 | Error ->
73 | Error
74 | end
75 | end.
76 | subscribe(ConfigName) ->
77 | Key = {sub, ConfigName, self()},
78 | case ets:insert_new(?TAB, {Key, self()}) of
79 | false -> ok;
80 | true ->
81 | ets:insert(?TAB, {{self(), ConfigName}, Key}),
82 | %% maybe monitor the process
83 | case ets:insert_new(?TAB, {self(), m}) of
84 | false -> ok;
85 | true ->
86 | gen_server:cast(?MODULE, {monitor_sub, self()})
87 | end
88 | end.
89 |
90 | %% @doc Remove subscribtion created using `subscribe(ConfigName)'
91 | unsubscribe(ConfigName) ->
92 | gen_server:call(?MODULE, {unsub, ConfigName}).
93 |
94 |
95 | %% @doc reload the configuration
96 | reload(ConfigName) ->
97 | reload(ConfigName, nil).
98 |
99 | %% @doc reload the configuration
100 | reload(ConfigName, IniFiles) ->
101 | gen_server:call(?MODULE, {reload, {ConfigName, IniFiles}}, infinity).
102 |
103 | start_autoreload(ConfigName) ->
104 | gen_server:call(?MODULE, {start_autoreload, ConfigName}).
105 |
106 | stop_autoreload(ConfigName) ->
107 | gen_server:call(?MODULE, {stop_autoreload, ConfigName}).
108 |
109 |
110 | %% @doc get all values of a configuration
111 | all(ConfigName) ->
112 | Matches = ets:match(?TAB, {{conf_key(ConfigName), '$1', '$2'}, '$3'}),
113 | [{Section, Key, Value} || [Section, Key, Value] <- Matches].
114 |
115 | %% @doc get all sections of a configuration
116 | sections(ConfigName) ->
117 | Matches = ets:match(?TAB, {{conf_key(ConfigName), '$1', '_'}, '_'}),
118 | lists:umerge(Matches).
119 |
120 |
121 | %% @doc get all sections starting by Prefix
122 | prefix(ConfigName, Prefix) ->
123 | Matches = ets:match(?TAB, {{conf_key(ConfigName), '$1', '_'}, '_'}),
124 | Found = lists:foldl(fun([Match], Acc) ->
125 | case lists:prefix(Prefix, Match) of
126 | true -> [Match | Acc];
127 | false -> Acc
128 | end
129 | end, [], Matches),
130 | lists:reverse(lists:usort(Found)).
131 |
132 | %% @doc retrive config as a proplist
133 | cfg2list(ConfigName) ->
134 | Matches = ets:match(?TAB, {{conf_key(ConfigName), '$1', '$2'}, '$3'}),
135 | lists:foldl(fun([Section, Key, Value], Props) ->
136 | case lists:keyfind(Section, 1, Props) of
137 | false ->
138 | [{Section, [{Key, Value}]} | Props];
139 | {Section, KVs} ->
140 | KVs1 = lists:keymerge(1, KVs, [{Key, Value}]),
141 | lists:keyreplace(Section, 1, Props, {Section, KVs1})
142 | end
143 | end, [], Matches).
144 |
145 | %% @doc retrieve config as a proplist
146 | cfg2list(ConfigName, GroupKey) ->
147 | Matches = ets:match(?TAB, {{conf_key(ConfigName), '$1', '$2'}, '$3'}),
148 | lists:foldl(fun([Section, Key, Value], Props) ->
149 | case re:split(Section, GroupKey, [{return,list}]) of
150 | [Section] ->
151 | case lists:keyfind(Section, 1, Props) of
152 | false ->
153 | [{Section, [{Key, Value}]} | Props];
154 | {Section, KVs} ->
155 | KVs1 = lists:keymerge(1, KVs, [{Key, Value}]),
156 | lists:keyreplace(Section, 1, Props, {Section, KVs1})
157 | end;
158 | [Section1, SSection] ->
159 | case lists:keyfind(Section1, 1, Props) of
160 | false ->
161 | [{Section1, [{SSection, [{Key, Value}]}]}
162 | | Props];
163 | {Section1, KVs} ->
164 | KVs1 = case lists:keyfind(SSection, 1, KVs) of
165 | false ->
166 | [{SSection, [{Key, Value}]} | KVs];
167 | {SSection, SKVs} ->
168 | SKVs1 = lists:keymerge(1, SKVs,
169 | [{Key, Value}]),
170 | lists:keyreplace(SSection, 1,
171 | KVs, {SSection,
172 | SKVs1})
173 | end,
174 | lists:keyreplace(Section1, 1, Props,
175 | {Section1, KVs1})
176 | end
177 | end
178 | end, [], Matches).
179 |
180 | %% @doc get values of a section
181 | get_value(ConfigName, Section0) ->
182 | Section = econfig_util:to_list(Section0),
183 | Matches = ets:match(?TAB, {{conf_key(ConfigName), Section, '$1'}, '$2'}),
184 | [{Key, Value} || [Key, Value] <- Matches].
185 |
186 | %% @doc get value for a key in a section
187 | get_value(ConfigName, Section, Key) ->
188 | get_value(ConfigName, Section, Key, undefined).
189 |
190 | get_value(ConfigName, Section0, Key0, Default) ->
191 | Section = econfig_util:to_list(Section0),
192 | Key = econfig_util:to_list(Key0),
193 |
194 | case ets:lookup(?TAB, {conf_key(ConfigName), Section, Key}) of
195 | [] -> Default;
196 | [{_, Match}] -> Match
197 | end.
198 |
199 | %% @doc set a section
200 | set_value(ConfigName, Section, List) ->
201 | set_value(ConfigName, Section, List, true).
202 |
203 | set_value(ConfigName, Section0, List, Persist)
204 | when is_boolean(Persist) ->
205 | Section = econfig_util:to_list(Section0),
206 | List1 = [{econfig_util:to_list(K), econfig_util:to_list(V)}
207 | || {K, V} <- List],
208 | gen_server:call(?MODULE, {mset, {ConfigName, Section, List1,
209 | Persist}}, infinity).
210 |
211 | set_value(ConfigName, Section0, Key0, Value0, Persist) ->
212 | Section = econfig_util:to_list(Section0),
213 | Key = econfig_util:to_list(Key0),
214 | Value = econfig_util:to_list(Value0),
215 | gen_server:call(?MODULE, {set, {ConfigName, Section, Key, Value, Persist}}, infinity).
216 |
217 | delete_value(ConfigName, Section) ->
218 | delete_value(ConfigName, Section, true).
219 |
220 | delete_value(ConfigName, Section0, Persist) when is_boolean(Persist) ->
221 | Section = econfig_util:to_list(Section0),
222 | gen_server:call(?MODULE, {mdel, {ConfigName, Section, Persist}},
223 | infinity);
224 |
225 | %% @doc delete a value
226 | delete_value(ConfigName, Section, Key) ->
227 | delete_value(ConfigName, Section, Key, true).
228 |
229 | delete_value(ConfigName, Section0, Key0, Persist) ->
230 | Section = econfig_util:to_list(Section0),
231 | Key = econfig_util:to_list(Key0),
232 |
233 | gen_server:call(?MODULE, {del, {ConfigName, Section, Key,
234 | Persist}}, infinity).
235 |
236 |
237 | init_tabs() ->
238 | case ets:info(?TAB, name) of
239 | undefined ->
240 | ets:new(?TAB, [ordered_set, public, named_table,
241 | {read_concurrency, true},
242 | {write_concurrency, true}]);
243 | _ ->
244 | true
245 | end.
246 |
247 | %% -----------------------------------------------
248 | %% gen_server callbacks
249 | %% -----------------------------------------------
250 |
251 | init(_) ->
252 | process_flag(trap_exit, true),
253 | InitialState = initialize_app_confs(),
254 | {ok, InitialState}.
255 |
256 | handle_call({register_conf, {ConfName, IniFiles, Options}}, _From, #state{confs=Confs}=State) ->
257 | {Resp, NewState} =
258 | try
259 | WriteFile = parse_inis(ConfName, IniFiles),
260 | {ok, Pid} = case proplists:get_value(autoreload, Options) of
261 | true ->
262 | Res = econfig_watcher_sup:start_watcher(ConfName, IniFiles),
263 | Res;
264 | Delay when is_integer(Delay) ->
265 | econfig_watcher_sup:start_watcher(ConfName, IniFiles, Delay);
266 | _ ->
267 | {ok, nil}
268 | end,
269 |
270 | ChangeFun = proplists:get_value(change_fun, Options, fun(_Change) -> ok end),
271 | ok = check_fun(ChangeFun),
272 | Confs1 = dict:store(ConfName, #config{write_file=WriteFile,
273 | pid=Pid,
274 | change_fun=ChangeFun,
275 | options=Options,
276 | inifiles=IniFiles},
277 | Confs),
278 | State2 = State#state{confs=Confs1},
279 | notify_change(State2, ConfName, registered),
280 | {ok, State2#state{confs=Confs1}}
281 | catch _Tag:Error ->
282 | {{error, Error}, State}
283 | end,
284 | {reply, Resp, NewState};
285 |
286 | handle_call({unregister_conf, ConfName}, _From, #state{confs=Confs}=State) ->
287 | true = ets:match_delete(?TAB, {{conf_key(ConfName), '_', '_'}, '_'}),
288 | case dict:find(ConfName, Confs) of
289 | {ok, #config{pid=Pid}} when is_pid(Pid) ->
290 | supervisor:terminate_child(econfig_watcher_sup, Pid),
291 | notify_change(State, ConfName, unregistered);
292 | _ ->
293 | ok
294 | end,
295 | {reply, ok, State#state{confs=dict:erase(ConfName, Confs)}};
296 |
297 | handle_call({reload, {ConfName, IniFiles0}}, _From,
298 | #state{confs=Confs}=State) ->
299 |
300 | case dict:find(ConfName, Confs) of
301 | {ok, #config{inifiles=IniFiles1, options=Options}=Conf} ->
302 |
303 | true = ets:match_delete(?TAB, {{conf_key(ConfName), '_', '_'}, '_'}),
304 | IniFiles = case IniFiles0 of
305 | nil -> IniFiles1;
306 | _ -> IniFiles0
307 | end,
308 |
309 | %% do the reload
310 | WriteFile = parse_inis(ConfName, IniFiles),
311 | Confs1 = dict:store(ConfName, Conf#config{write_file=WriteFile,
312 | options=Options,
313 | inifiles=IniFiles},
314 | Confs),
315 | State2 = State#state{confs=Confs1},
316 | notify_change(State2, ConfName, reload),
317 | {reply, ok, State2};
318 | _ ->
319 | {reply, ok, State}
320 | end;
321 |
322 | handle_call({start_autoreload, ConfName}, _From, #state{confs=Confs}=State) ->
323 | case dict:find(ConfName, Confs) of
324 | {ok, #config{inifiles=IniFiles}=Config} ->
325 | {ok, Pid} = econfig_watcher_sup:start_watcher(ConfName, IniFiles),
326 | Config1 = Config#config{pid=Pid},
327 | {reply, ok, State#state{confs=dict:store(ConfName, Config1, Confs)}};
328 | _ ->
329 | {reply, ok, State}
330 | end;
331 |
332 | handle_call({stop_autoreload, ConfName}, _From, #state{confs=Confs}=State) ->
333 | case dict:find(ConfName, Confs) of
334 | {ok, #config{pid=Pid}=Config} when is_pid(Pid) ->
335 | supervisor:terminate_child(econfig_watcher_sup, Pid),
336 | Config1 = Config#config{pid=nil},
337 | {reply, ok, State#state{confs=dict:store(ConfName, Config1,
338 | Confs)}};
339 | _ ->
340 | {reply, ok, State}
341 | end;
342 |
343 |
344 | handle_call({set, {ConfName, Section, Key, Value, Persist}}, _From,
345 | #state{confs=Confs}=State) ->
346 |
347 | Result = case {Persist, dict:find(ConfName, Confs)} of
348 | {true, {ok, #config{write_file=FileName}=Conf}} when FileName /= nil->
349 | maybe_pause(Conf, fun() ->
350 | econfig_file_writer:save_to_file({Section, [{Key, Value}]}, FileName)
351 | end);
352 | _ ->
353 | ok
354 | end,
355 | case Result of
356 | ok ->
357 | Value1 = econfig_util:trim_whitespace(Value),
358 | case Value1 of
359 | [] ->
360 | true = ets:delete(?TAB, {conf_key(ConfName), Section, Key});
361 | _ ->
362 | true = ets:insert(?TAB, {{conf_key(ConfName), Section, Key}, Value1})
363 | end,
364 | notify_change(State, ConfName, {set, {Section, Key}}),
365 | {reply, ok, State};
366 | _Error ->
367 | {reply, Result, State}
368 | end;
369 | handle_call({mset, {ConfName, Section, List, Persist}}, _From,
370 | #state{confs=Confs}=State) ->
371 | Result = case {Persist, dict:find(ConfName, Confs)} of
372 | {true, {ok, #config{write_file=FileName}=Conf}} when FileName /= nil->
373 | maybe_pause(Conf, fun() ->
374 | econfig_file_writer:save_to_file({Section, List}, FileName)
375 | end);
376 | _ ->
377 | ok
378 | end,
379 | case Result of
380 | ok ->
381 | lists:foreach(fun({Key,Value}) ->
382 | Value1 = econfig_util:trim_whitespace(Value),
383 | if
384 | Value1 /= [] ->
385 | true = ets:insert(?TAB, {{conf_key(ConfName), Section, Key}, Value1}),
386 | notify_change(State, ConfName, {set, {Section, Key}});
387 | true ->
388 | true = ets:delete(?TAB, {conf_key(ConfName), Section, Key}),
389 | notify_change(State, ConfName, {delete, {Section, Key}})
390 | end
391 | end, List),
392 | {reply, ok, State};
393 | _Error ->
394 | {reply, Result, State}
395 | end;
396 |
397 | handle_call({del, {ConfName, Section, Key, Persist}}, _From,
398 | #state{confs=Confs}=State) ->
399 |
400 | true = ets:delete(?TAB, {conf_key(ConfName), Section, Key}),
401 |
402 | case {Persist, dict:find(ConfName, Confs)} of
403 | {true, {ok, #config{write_file=FileName}=Conf}} when FileName /= nil->
404 | maybe_pause(Conf, fun() ->
405 | econfig_file_writer:save_to_file({Section, [{Key, ""}]}, FileName)
406 | end);
407 | _ ->
408 | ok
409 | end,
410 | notify_change(State, ConfName, {delete, {Section, Key}}),
411 | {reply, ok, State};
412 | handle_call({mdel, {ConfName, Section, Persist}}, _From,
413 | #state{confs=Confs}=State) ->
414 | Matches = ets:match(?TAB, {{conf_key(ConfName), Section, '$1'}, '$2'}),
415 | ToDelete = lists:foldl(fun([Key, _Val], Acc) ->
416 | true = ets:delete(?TAB, {conf_key(ConfName), Section, Key}),
417 | notify_change(State, ConfName, {delete, {Section, Key}}),
418 | [{Key, ""} | Acc]
419 | end, [], Matches),
420 |
421 | case {Persist, dict:find(ConfName, Confs)} of
422 | {true, {ok, #config{write_file=FileName}=Conf}} when FileName /= nil->
423 | maybe_pause(Conf, fun() ->
424 | econfig_file_writer:save_to_file({Section, ToDelete}, FileName)
425 | end);
426 | _ ->
427 | ok
428 | end,
429 | {reply, ok, State};
430 |
431 | handle_call({unsub, ConfName}, {Pid, _}, State) ->
432 | Key = {sub, ConfName, Pid},
433 | case ets:lookup(?TAB, Key) of
434 | [{Key, Pid}] ->
435 | _ = ets:delete(?TAB, Key),
436 | _ = ets:delete(?TAB, {Pid, ConfName});
437 | [] -> ok
438 | end,
439 | {reply, ok, State};
440 |
441 | handle_call(_Msg, _From, State) ->
442 | {reply, ok, State}.
443 |
444 | handle_cast({monitor_sub, Pid}, State) ->
445 | erlang:monitor(process, Pid),
446 | {noreply, State};
447 |
448 | handle_cast(_Msg, State) ->
449 | {noreply, State}.
450 |
451 |
452 | handle_info({'DOWN', _MRef, process, Pid, _}, State) ->
453 | _ = process_is_down(Pid),
454 | {noreply, State};
455 |
456 | handle_info(_Info, State) ->
457 | {noreply, State}.
458 |
459 | code_change(_OldVsn, State, _Extra) ->
460 | {ok, State}.
461 |
462 | terminate(_Reason , _State) ->
463 | ok.
464 |
465 |
466 | conf_key(Name) -> {c, Name}.
467 |
468 | %% -----------------------------------------------
469 | %% internal functions
470 | %% -----------------------------------------------
471 | %%
472 |
473 | maybe_pause(#config{pid=Pid}, Fun) when is_pid(Pid) ->
474 | econfig_watcher:pause(Pid),
475 | Fun(),
476 | econfig_watcher:restart(Pid);
477 | maybe_pause(_, Fun) ->
478 | Fun().
479 |
480 | notify_change(State, ConfigName, Event) ->
481 | Msg = {config_updated, ConfigName, Event},
482 | run_change_fun(State, ConfigName, Msg),
483 | send(ConfigName, Msg).
484 |
485 | send(ConfigName, Msg) ->
486 | Subs = ets:select(?TAB, [{{{sub, ConfigName, '_'}, '$1'}, [], ['$1']}]),
487 | lists:foreach(fun(Pid) ->
488 | catch Pid ! Msg
489 | end, Subs).
490 |
491 | run_change_fun(State, ConfigName, Msg) ->
492 | {ok, #config{change_fun=ChangeFun}} = dict:find(ConfigName, State#state.confs),
493 | Ret = (catch apply_change_fun(ChangeFun, Msg)),
494 | case Ret of
495 | {'EXIT', Reason} ->
496 | error_logger:warning_msg("~p~n error running change hook: ~p~n", [?MODULE, Reason]),
497 | ok;
498 | _ ->
499 | ok
500 | end.
501 |
502 | apply_change_fun(none, _Msg) -> ok;
503 | apply_change_fun({M, F}, Msg) -> apply(M, F, [Msg]);
504 | apply_change_fun(F, Msg) -> F(Msg).
505 |
506 |
507 | initialize_app_confs() ->
508 | case application:get_env(econfig, confs) of
509 | undefined ->
510 | #state{};
511 | {ok, Confs} ->
512 | initialize_app_confs1(Confs, #state{})
513 | end.
514 |
515 | initialize_app_confs1([], State) ->
516 | State;
517 | initialize_app_confs1([{ConfName, IniFiles} | Rest], State) ->
518 | initialize_app_confs1([{ConfName, IniFiles, []} | Rest], State);
519 | initialize_app_confs1([{ConfName, IniFiles, Options} | Rest],
520 | #state{confs=Confs}=State) ->
521 | WriteFile = parse_inis(ConfName, IniFiles),
522 | {ok, Pid} = case proplists:get_value(autoreload, Options) of
523 | true ->
524 | econfig_watcher_sup:start_watcher(ConfName, IniFiles);
525 | _ ->
526 | {ok, nil}
527 | end,
528 | Confs1 = dict:store(ConfName, #config{write_file=WriteFile,
529 | pid=Pid,
530 | options=Options,
531 | inifiles=IniFiles},
532 | Confs),
533 |
534 | initialize_app_confs1(Rest, State#state{confs=Confs1}).
535 |
536 |
537 |
538 | parse_inis(ConfName, IniFiles0) ->
539 | IniFiles = econfig_util:find_files(IniFiles0),
540 | lists:map(fun(IniFile) ->
541 | {ok, ParsedIniValues, DelKeys} = parse_ini_file(ConfName, IniFile),
542 | ets:insert(?TAB, ParsedIniValues),
543 | lists:foreach(fun(Key) -> ets:delete(?TAB, Key) end, DelKeys)
544 | end, IniFiles),
545 | WriteFile = lists:last(IniFiles),
546 | WriteFile.
547 |
548 |
549 | parse_ini_file(ConfName, IniFile) ->
550 | IniFilename = econfig_util:abs_pathname(IniFile),
551 | IniBin =
552 | case file:read_file(IniFilename) of
553 | {ok, IniBin0} ->
554 | IniBin0;
555 | {error, eacces} ->
556 | throw({file_permission_error, IniFile});
557 | {error, enoent} ->
558 | Fmt = "Couldn't find server configuration file ~s.",
559 | Msg = list_to_binary(io_lib:format(Fmt, [IniFilename])),
560 | throw({startup_error, Msg})
561 | end,
562 |
563 | Lines = re:split(IniBin, "\r\n|\n|\r|\032", [{return, list}]),
564 | {_, ParsedIniValues, DeleteIniKeys} =
565 | lists:foldl(fun(Line, {AccSectionName, AccValues, AccDeletes}) ->
566 | case string:strip(Line) of
567 | "[" ++ Rest ->
568 | case re:split(Rest, "\\]", [{return, list}]) of
569 | [NewSectionName, ""] ->
570 | {NewSectionName, AccValues, AccDeletes};
571 | _Else -> % end bracket not at end, ignore this line
572 | {AccSectionName, AccValues, AccDeletes}
573 | end;
574 | ";" ++ _Comment ->
575 | {AccSectionName, AccValues, AccDeletes};
576 | Line2 ->
577 | case re:split(Line2, "\s*=\s*", [{return, list}]) of
578 | [Value] ->
579 | MultiLineValuePart = case re:run(Line, "^ \\S", []) of
580 | {match, _} ->
581 | true;
582 | _ ->
583 | false
584 | end,
585 | case {MultiLineValuePart, AccValues} of
586 | {true, [{{_, ValueName}, PrevValue} | AccValuesRest]} ->
587 | % remove comment
588 | case re:split(Value, "\s*;|\t;", [{return, list}]) of
589 | [[]] ->
590 | % empty line
591 | {AccSectionName, AccValues, AccDeletes};
592 | [LineValue | _Rest] ->
593 | E = {{AccSectionName, ValueName},
594 | PrevValue ++ " " ++
595 | econfig_util:trim_whitespace(LineValue)},
596 | {AccSectionName, [E | AccValuesRest], AccDeletes}
597 | end;
598 | _ ->
599 | {AccSectionName, AccValues, AccDeletes}
600 | end;
601 | [""|_LineValues] -> % line begins with "=", ignore
602 | {AccSectionName, AccValues, AccDeletes};
603 | [ValueName|LineValues] -> % yeehaw, got a line!
604 | %% replace all tabs by an empty value.
605 | ValueName1 = econfig_util:trim_whitespace(ValueName),
606 | RemainingLine = econfig_util:implode(LineValues, "="),
607 | % removes comments
608 | case re:split(RemainingLine, "\s*;|\t;", [{return, list}]) of
609 | [[]] ->
610 | % empty line means delete this key
611 | AccDeletes1 = [{conf_key(ConfName), AccSectionName, ValueName1}
612 | | AccDeletes],
613 | {AccSectionName, AccValues, AccDeletes1};
614 | [LineValue | _Rest] ->
615 | {AccSectionName,
616 | [{{conf_key(ConfName), AccSectionName, ValueName1},
617 | econfig_util:trim_whitespace(LineValue)}
618 | | AccValues], AccDeletes}
619 | end
620 | end
621 | end
622 | end, {"", [], []}, Lines),
623 | {ok, ParsedIniValues, DeleteIniKeys}.
624 |
625 | process_is_down(Pid) when is_pid(Pid) ->
626 | case ets:member(?TAB, Pid) of
627 | false ->
628 | ok;
629 | true ->
630 | Subs = ets:select(?TAB, [{{{Pid, '$1'}, '$2'}, [], [{{'$1', '$2'}}]}]),
631 | lists:foreach(fun({ConfName, SubKey}) ->
632 | ets:delete(?TAB, {Pid, ConfName}),
633 | ets:delete(?TAB, SubKey)
634 | end, Subs),
635 | ets:delete(?TAB, Pid),
636 | ok
637 | end.
638 |
639 | check_fun(none) ->
640 | ok;
641 | check_fun(Fun) when is_function(Fun) ->
642 | case erlang:fun_info(Fun, arity) of
643 | {arity, 1} -> ok;
644 | _ -> {error, badarity}
645 | end;
646 | check_fun({Mod, Fun}) ->
647 | _ = code:ensure_loaded(Mod),
648 | case erlang:function_exported(Mod, Fun, 1) of
649 | true -> ok;
650 | false -> {error, function_not_exported}
651 | end.
652 |
--------------------------------------------------------------------------------
/src/econfig_sup.erl:
--------------------------------------------------------------------------------
1 | %%% -*- erlang -*-
2 | %%%
3 | %%% This file is part of econfig released under the Apache 2 license.
4 | %%% See the NOTICE for more information.
5 |
6 | %% @hidden
7 |
8 | -module(econfig_sup).
9 |
10 | -behaviour(supervisor).
11 |
12 | %% API
13 | -export([start_link/0]).
14 |
15 | %% Supervisor callbacks
16 | -export([init/1]).
17 |
18 | %% ===================================================================
19 | %% API functions
20 | %% ===================================================================
21 |
22 | start_link() ->
23 | supervisor:start_link({local, ?MODULE}, ?MODULE, []).
24 |
25 | %% ===================================================================
26 | %% Supervisor callbacks
27 | %% ===================================================================
28 |
29 | init([]) ->
30 | %% main config server
31 | Server = {econfig_server,
32 | {econfig_server, start_link, []},
33 | permanent, 5000, worker, [econfig_server]},
34 |
35 | %% watchr supervisor spec
36 | WatcherSup = {econfig_watcher_sup,
37 | {econfig_watcher_sup, start_link, []},
38 | permanent, infinity, supervisor, [econfig_watcher_sup]},
39 |
40 | {ok, { {one_for_one, 5, 10}, [Server, WatcherSup]} }.
41 |
42 |
--------------------------------------------------------------------------------
/src/econfig_util.erl:
--------------------------------------------------------------------------------
1 | %%% -*- erlang -*-
2 | %%%
3 | %%% This file is part of econfig released under the Apache 2 license.
4 | %%% See the NOTICE for more information.
5 |
6 | %% @hidden
7 |
8 | -module(econfig_util).
9 |
10 | -export([find_ini_files/1,
11 | find_files/1, find_files/2, find_files/3,
12 | implode/2,
13 | abs_pathname/1,
14 | to_list/1,
15 | trim_whitespace/1]).
16 |
17 |
18 | find_ini_files(Path) ->
19 | {ok, Files} = file:list_dir(Path),
20 | IniFiles = [filename:join(Path, Name) || Name <- Files,
21 | ".ini" =:= filename:extension(Name)],
22 | lists:usort(IniFiles).
23 |
24 |
25 | find_files(Paths) ->
26 | find_files(Paths, [], fun(F) -> F end).
27 |
28 | find_files(Paths, Fun) ->
29 | find_files(Paths, [], Fun).
30 |
31 | find_files([], Acc, _Fun) ->
32 | Acc;
33 | find_files([Path | Rest], Acc, Fun) ->
34 | case filelib:is_dir(Path) of
35 | true ->
36 | IniFiles = econfig_util:find_ini_files(Path),
37 | Acc1 = Acc ++ lists:map(Fun, IniFiles),
38 | find_files(Rest, Acc1, Fun);
39 | false ->
40 | Acc1 = Acc ++ [Fun(Path)],
41 | find_files(Rest, Acc1, Fun)
42 | end.
43 |
44 |
45 | implode(List, Sep) ->
46 | implode(List, Sep, []).
47 |
48 | implode([], _Sep, Acc) ->
49 | lists:flatten(lists:reverse(Acc));
50 | implode([H], Sep, Acc) ->
51 | implode([], Sep, [H|Acc]);
52 | implode([H|T], Sep, Acc) ->
53 | implode(T, Sep, [Sep,H|Acc]).
54 |
55 | % given a pathname "../foo/bar/" it gives back the fully qualified
56 | % absolute pathname.
57 | abs_pathname(" " ++ Filename) ->
58 | % strip leading whitespace
59 | abs_pathname(Filename);
60 | abs_pathname([$/ |_]=Filename) ->
61 | Filename;
62 | abs_pathname(Filename) ->
63 | {ok, Cwd} = file:get_cwd(),
64 | {Filename2, Args} = separate_cmd_args(Filename, ""),
65 | abs_pathname(Filename2, Cwd) ++ Args.
66 |
67 | abs_pathname(Filename, Dir) ->
68 | Name = filename:absname(Filename, Dir ++ "/"),
69 | OutFilename = filename:join(fix_path_list(filename:split(Name), [])),
70 | % If the filename is a dir (last char slash, put back end slash
71 | case string:right(Filename,1) of
72 | "/" ->
73 | OutFilename ++ "/";
74 | "\\" ->
75 | OutFilename ++ "/";
76 | _Else->
77 | OutFilename
78 | end.
79 |
80 | to_list(V) when is_list(V) ->
81 | V;
82 | to_list(V) when is_binary(V) ->
83 | binary_to_list(V);
84 | to_list(V) when is_atom(V) ->
85 | atom_to_list(V);
86 | to_list(V) when is_integer(V) ->
87 | integer_to_list(V);
88 | to_list(V) when is_float(V) ->
89 | io_lib:format("~.3f", [V]);
90 | to_list(V) ->
91 | lists:flatten(io_lib:format("~p", [V])).
92 |
93 | %% @doc trims whitespace
94 | trim_whitespace(Value) ->
95 | re:replace(
96 | re:replace(Value, "^[\s\t]+", "", [{return, list}, global]),
97 | "[\s\t]+$", "", [{return, list}, global]).
98 |
99 | %% --
100 | %% private functions
101 | %%
102 |
103 | %% @doc takes a heirarchical list of dirs and removes the dots ".", double dots
104 | %% ".." and the corresponding parent dirs.
105 | fix_path_list([], Acc) ->
106 | lists:reverse(Acc);
107 | fix_path_list([".."|Rest], [_PrevAcc|RestAcc]) ->
108 | fix_path_list(Rest, RestAcc);
109 | fix_path_list(["."|Rest], Acc) ->
110 | fix_path_list(Rest, Acc);
111 | fix_path_list([Dir | Rest], Acc) ->
112 | fix_path_list(Rest, [Dir | Acc]).
113 |
114 | %% @doc if this as an executable with arguments, seperate out the arguments
115 | %% ""./foo\ bar.sh -baz=blah" -> {"./foo\ bar.sh", " -baz=blah"}
116 | separate_cmd_args("", CmdAcc) ->
117 | {lists:reverse(CmdAcc), ""};
118 | separate_cmd_args("\\ " ++ Rest, CmdAcc) -> % handle skipped value
119 | separate_cmd_args(Rest, " \\" ++ CmdAcc);
120 | separate_cmd_args(" " ++ Rest, CmdAcc) ->
121 | {lists:reverse(CmdAcc), " " ++ Rest};
122 | separate_cmd_args([Char|Rest], CmdAcc) ->
123 | separate_cmd_args(Rest, [Char | CmdAcc]).
124 |
--------------------------------------------------------------------------------
/src/econfig_watcher.erl:
--------------------------------------------------------------------------------
1 | %%% -*- erlang -*-
2 | %%%
3 | %%% This file is part of econfig released under the Apache 2 license.
4 | %%% See the NOTICE for more information.
5 |
6 | %% @hidden
7 |
8 | -module(econfig_watcher).
9 | -behaviour(gen_server).
10 |
11 | -export([start_link/2, start_link/3,
12 | pause/1, restart/1]).
13 |
14 | -export([init/1,
15 | handle_call/3,
16 | handle_cast/2,
17 | handle_info/2,
18 | terminate/2,
19 | code_change/3]).
20 |
21 | -record(watcher, {tc,
22 | files = [],
23 | scan_delay,
24 | conf,
25 | paths}).
26 |
27 | -record(file, {path, last_mod}).
28 |
29 | start_link(ConfName, Paths) ->
30 | start_link(ConfName, Paths, scan_delay()).
31 |
32 | start_link(ConfName, Paths, Delay) ->
33 | gen_server:start_link(?MODULE, [ConfName, Paths, Delay] , []).
34 |
35 |
36 | pause(Pid) ->
37 | gen_server:call(Pid, pause).
38 |
39 | restart(Pid) ->
40 | gen_server:call(Pid, restart).
41 |
42 | init([ConfName, Paths, Delay]) ->
43 | Files = econfig_util:find_files(Paths, fun file_info/1),
44 | Tc = erlang:start_timer(Delay, self(), scan),
45 | InitState = #watcher{tc=Tc,
46 | scan_delay=Delay,
47 | files=Files,
48 | conf=ConfName,
49 | paths=Paths},
50 | {ok, InitState}.
51 |
52 |
53 | handle_call(pause, _From, State=#watcher{tc=Tc}) when Tc /= nil ->
54 | erlang:cancel_timer(Tc),
55 | {reply, ok, State#watcher{tc=nil}};
56 | handle_call(restart, _From, State=#watcher{tc=nil, scan_delay=Delay,
57 | paths=Paths}) ->
58 | Files = econfig_util:find_files(Paths, fun file_info/1),
59 | Tc = erlang:start_timer(Delay, self(), scan),
60 | {reply, ok, State#watcher{tc=Tc, files=Files}};
61 |
62 | handle_call(_Req, _From, State) ->
63 | {reply, ok, State}.
64 |
65 | handle_cast(_Event, State) ->
66 | {noreply, State}.
67 |
68 |
69 | handle_info({timeout, Tc, scan}, State=#watcher{tc=Tc, scan_delay=Delay,
70 | files=OldFiles, conf=Conf,
71 | paths=Paths}) ->
72 | NewFiles = econfig_util:find_files(Paths, fun file_info/1),
73 | case OldFiles -- NewFiles of
74 | [] ->
75 | ok;
76 | _ ->
77 | IniFiles = lists:map(fun(#file{path=P}) -> P end, NewFiles),
78 | econfig_server:reload(Conf, IniFiles)
79 | end,
80 | {noreply, State#watcher{tc=erlang:start_timer(Delay, self(), scan),
81 | files=NewFiles}};
82 |
83 | handle_info(_Info, State) ->
84 | {noreply, State}.
85 |
86 | code_change(_OldVsn, State, _Extra) ->
87 | {ok, State}.
88 |
89 | terminate(_Reason, _State) ->
90 | ok.
91 |
92 |
93 | %% --
94 | %% internal functions
95 | %%
96 |
97 | scan_delay() ->
98 | case application:get_env(econfig, scan_delay) of
99 | undefined -> 5000;
100 | {ok, Delay} -> Delay
101 | end.
102 |
103 | file_info(Path) ->
104 | LastMod = filelib:last_modified(Path),
105 | #file{path=Path, last_mod=LastMod}.
106 |
--------------------------------------------------------------------------------
/src/econfig_watcher_sup.erl:
--------------------------------------------------------------------------------
1 | %%% -*- erlang -*-
2 | %%%
3 | %%% This file is part of econfig released under the Apache 2 license.
4 | %%% See the NOTICE for more information.
5 |
6 | %% @hidden
7 |
8 | -module(econfig_watcher_sup).
9 | -behaviour(supervisor).
10 |
11 | -export([start_watcher/2, start_watcher/3]).
12 | -export([start_link/0, stop/1]).
13 |
14 | -export([init/1]).
15 |
16 | start_watcher(ConfigName, Path) ->
17 | supervisor:start_child(?MODULE, [ConfigName, Path]).
18 |
19 | start_watcher(ConfigName, Path, Delay) ->
20 | supervisor:start_child(?MODULE, [ConfigName, Path, Delay]).
21 |
22 | start_link() ->
23 | supervisor:start_link({local, ?MODULE}, ?MODULE, []).
24 |
25 | stop(_S) -> ok.
26 |
27 | %% @private
28 | init([]) ->
29 | {ok,
30 | {{simple_one_for_one, 10, 10},
31 | [{econfig_watcher,
32 | {econfig_watcher, start_link, []},
33 | permanent, 5000, worker, [econfig_watcher]}]}}.
34 |
--------------------------------------------------------------------------------
/test/bootstrap_travis.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | curl -O -L https://s3.amazonaws.com/rebar3/rebar3
4 | chmod +x rebar3
5 | ./rebar3 update
--------------------------------------------------------------------------------
/test/fixtures/test.ini:
--------------------------------------------------------------------------------
1 | [section1]
2 | key1 = value1
3 | key2 = value 2
4 | key 3 = value 3
5 | key4 = value 4; this is a comment
6 | key5 = value5 ; this is a comment
7 |
8 | [section 2]
9 | key6 = value6
10 | key7 = value7
11 | key8 = value8
12 | key9 = value 9 ; another comment here
13 | key10 = value10 ; yet another comment
14 |
15 | [section3]
16 | ; this is a comment line
17 | key11 = new-val-11
18 | ; key11 = old-val-11
19 | ; key12 = old-val-12
20 | key13 = this is a value for key 13
21 | key14 = some-collection.of+random@characters
22 | key15 =
23 |
24 | [section4]
25 | key1 = 1
26 | key2 = true
27 | key3 = false
28 | key4 = a, b
29 | key5 = 1.4
30 | key6 = test
--------------------------------------------------------------------------------
/test/fixtures/test2.ini:
--------------------------------------------------------------------------------
1 | [section10]
2 | key1 = value1
3 | key2 = value 2
4 |
5 | [section 2]
6 | key3 = value3
7 | key6 = value6 overwrite
8 | key7 = value7
9 | key888 = value 888
10 |
11 | [section1]
12 | key6 = value6
13 |
--------------------------------------------------------------------------------