├── .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 | [![Build Status](https://travis-ci.org/benoitc/econfig.png?branch=master)](https://travis-ci.org/benoitc/econfig) 15 | [![Hex pm](http://img.shields.io/hexpm/v/econfig.svg?style=flat)](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 | [![Build Status](https://travis-ci.org/benoitc/econfig.png?branch=master)](https://travis-ci.org/benoitc/econfig) 15 | [![Hex pm](http://img.shields.io/hexpm/v/econfig.svg?style=flat)](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 |
all/1get all values of a configuration.
cfg2list/1retrive config as a proplist.
cfg2list/2retrieve config as a proplist.
delete_value/2delete all key/values from a section.
delete_value/3delete a value and persist the change to the file.
delete_value/4delete a value and optionnally persist it.
get_binary/3get a value and convert it to an binary.
get_binary/4get a value and convert it to an binary.
get_boolean/3get a value and convert it to a boolean if possible 82 | This method is case-insensitive and recognizes Boolean values from 'yes'/'no', 'on'/'off', 'true'/'false' and '1'/'0' 83 | a badarg error is raised if the value can't be parsed to a boolean.
get_boolean/4get a value and convert it to a boolean if possible.
get_float/3get a value and convert it to an float.
get_float/4get a value and convert it to an float.
get_integer/3get a value and convert it to an integer.
get_integer/4get a value and convert it to an integer.
get_list/3get a value and convert it to an list.
get_list/4get a value and convert it to an list.
get_value/2get keys/values of a section.
get_value/3get value for a key in a section.
get_value/4get value for a key in a section or return the default value if not set.
open_config/2open or create an ini file an register it.
open_config/3open or create an ini file an register it.
prefix/2get all sections starting by Prefix.
register_config/2register inifiles or config dirs.
register_config/3register inifiles of config dirs with options 84 | For now the only option isautoreload to auto reload the config on 85 | files or dirs changes.
reload/1reload the configuration.
reload/2reload the configuration.
sections/1get all sections of a configuration.
set_value/3set a list of key/value for a section.
set_value/4set a value and persist it to the file.
set_value/5set a value and optionnaly persist it.
start_autoreload/1start the config watcher.
stop_autoreload/1stop the config watcher.
subscribe/1Subscribe to config events for a config named ConfigName
unregister_config/1unregister a conf.
unsubscribe/1Remove all subscribtions to ConfigName events for this process.
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 | [![Build Status](https://travis-ci.org/benoitc/econfig.png?branch=master)](https://travis-ci.org/benoitc/econfig) 27 | [![Hex pm](http://img.shields.io/hexpm/v/econfig.svg?style=flat)](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 | --------------------------------------------------------------------------------