├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── elvis.config ├── rebar.config ├── src ├── nconf.app.src └── nconf.erl └── test ├── nconf_meta_SUITE.erl └── nconf_test.erl /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.o 3 | *.plt 4 | .eunit 5 | .rebar 6 | deps 7 | ebin 8 | erl_crash.dump 9 | rel/example_project 10 | xrefr 11 | .erlang.mk/ 12 | _build/ 13 | logs/ 14 | nconf.d 15 | nconf_config_test_file.nconf 16 | rebar.lock 17 | doc 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.0.1](https://github.com/inaka/nconf/tree/0.0.1) (2016-08-16) 4 | **Closed issues:** 5 | 6 | - Move from erlang.mk to rebar3 [\#12](https://github.com/inaka/nconf/issues/12) 7 | - Update repo and make it ready for hex.pm [\#10](https://github.com/inaka/nconf/issues/10) 8 | - Hex Package [\#9](https://github.com/inaka/nconf/issues/9) 9 | - Bring in the original code [\#2](https://github.com/inaka/nconf/issues/2) 10 | - Fix LICENSE [\#1](https://github.com/inaka/nconf/issues/1) 11 | 12 | **Merged pull requests:** 13 | 14 | - \[Close \#12\] rebar3 as a build tool [\#13](https://github.com/inaka/nconf/pull/13) ([Euen](https://github.com/Euen)) 15 | - \[Fix \#10\] Update erlang.mk; Add meta testing; Add rebar.config file [\#11](https://github.com/inaka/nconf/pull/11) ([harenson](https://github.com/harenson)) 16 | - README: Add example and details [\#8](https://github.com/inaka/nconf/pull/8) ([hcs42](https://github.com/hcs42)) 17 | - Igaray.setup [\#7](https://github.com/inaka/nconf/pull/7) ([igaray](https://github.com/igaray)) 18 | - \[\#2\] Add basic nconf implementation and unit test [\#6](https://github.com/inaka/nconf/pull/6) ([hcs42](https://github.com/hcs42)) 19 | - Fix \#1 - LICENSE update [\#4](https://github.com/inaka/nconf/pull/4) ([elbrujohalcon](https://github.com/elbrujohalcon)) 20 | 21 | 22 | 23 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2015 Erlang Solutions Ltd. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nconf 2 | 3 | Nested Configuration Manager for Erlang Applications 4 | 5 | `nconf` is a module for reading the contents of an nconf config file and 6 | modifying the configuration parameters accordingly. 7 | 8 | The following commands are available in an nconf config file: 9 | 10 | ```erlang 11 | {set, AppName, ParamName, Path1, ..., PathN, Replacement} 12 | {replace, AppName, ParamName, Path1, ..., PathN, Replacement} 13 | {unset, AppName, ParamName, Path1, ..., PathN} 14 | ``` 15 | 16 | ## Example 17 | 18 | Assume that we have an application that configures some ports and passwords to 19 | be used in `sys.config`: 20 | 21 | ```erlang 22 | [{myapp, 23 | [{ports, 24 | [{ftp, 21}, 25 | {ssh, 22}, 26 | {http, 80}, 27 | {ssl, 443}, 28 | {snmp, 161}], 29 | {passwords, 30 | [{"user1", "pw1"}, 31 | {"user2", "pw2"}]}}]. 32 | ``` 33 | 34 | **How can users modify the settings**? Let's say a user wants to set the http 35 | port to 8080, delete the second password and add a third one. The options are the 36 | following: 37 | 38 | 1. The user can modify the `sys.config` file itself. This is often not 39 | desirable, e.g. when the sys.config is a generated file, or when the next 40 | release of our application might contain a different sys.config file with 41 | additional default values. 42 | 43 | 2. The user can create a `user.config` file, use the [-config][1] option when 44 | starting `erl`, and thus have the configuration described in `sys.config` 45 | updated with the contents of `user.config`. The problem is that configuration 46 | values in the `user.config` file will overwrite the values in the 47 | `sys.config` files instead of being merged. So the user would need to repeat 48 | each configuration parameter in `sys.config` that needs to be updated (such 49 | as `ports` and `passwords`): 50 | 51 | ```erlang 52 | [{myapp, 53 | [{ports, 54 | [{ftp, 21}, 55 | {ssh, 22}, 56 | {http, 8080}, 57 | {ssl, 443}, 58 | {snmp, 161}], 59 | {passwords, 60 | [{"user1", "pw1"}, 61 | {"user3", "pw3"}]}}]. 62 | ``` 63 | 64 | 3. Finally, **the user can use nconf** and write the following nconf 65 | configuration file: 66 | 67 | ```erlang 68 | {set, myapp, ports, http, 80}. 69 | {unset, myapp, passwords, "user2"}. 70 | {set, myapp, passwords, "user3", "pw3"}. 71 | ``` 72 | 73 | [1]: http://www.erlang.org/doc/man/config.html 74 | 75 | ## Details 76 | 77 | The configuration entries are organized into a tree. The nconf config file 78 | contains a number of tuples, each of which modifies one leaf or branch of this 79 | tree. Leaves and branches can be replaced (with the `set` and `replace` 80 | commands) and deleted (with the `unset` command). The elements of the tuple 81 | (after the command) specify the path from the root of the configuration tree 82 | towards the leaf or branch to be modified or deleted. In case of the `set` and 83 | `replace` commands, the last element of the tuple defines the new value for the 84 | leaf or branch. 85 | 86 | The available commands in an nconf config file are the following: 87 | 88 | ```erlang 89 | {set, AppName, ParamName, Path1, ..., PathN, Replacement}. 90 | ``` 91 | 92 | Sets the value of a given configuration entry to the given value. 93 | 94 | ```erlang 95 | {replace, AppName, ParamName, Path1, ..., PathN, Replacement}. 96 | ``` 97 | 98 | Sets a given configuration entry to the given value. The difference between 99 | `set` and `replace` is that the former sets only the value, while the latter the 100 | entry itself. Compare `{set, myapp, myparam, a, 1}` and `{replace, myapp, 101 | myparam, a, {a, 1}}`, which perform the same task. The latter can be used to 102 | set not only a pair, but also a longer tuple as a configuration entry; for 103 | example the effect of `{replace, myapp, myparam, a, {a, 1, 2}}` cannot be 104 | reproduced with `set`. Generally it is a good practice to use `set` whenever 105 | possible, and use `replace` only when necessary. 106 | 107 | ```erlang 108 | {unset, AppName, ParamName, Path1, ..., PathN}. 109 | ``` 110 | 111 | Deletes the given configuration entry. 112 | -------------------------------------------------------------------------------- /elvis.config: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | elvis, 4 | [ 5 | {config, 6 | [#{dirs => ["src"], 7 | filter => "*.erl", 8 | ruleset => erl_files 9 | }, 10 | #{dirs => ["test"], 11 | filter => "*.erl", 12 | rules => [{elvis_style, macro_names, disable}], 13 | ruleset => erl_files 14 | }, 15 | #{dirs => ["."], 16 | filter => "rebar.config", 17 | ruleset => rebar_config 18 | }, 19 | #{dirs => ["."], 20 | filter => "elvis.config", 21 | ruleset => elvis_config 22 | } 23 | ] 24 | } 25 | ] 26 | } 27 | ]. 28 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang;erlang-indent-level: 2;indent-tabs-mode: nil -*- 2 | %% ex: ts=4 sw=4 ft=erlang et 3 | 4 | %% == Erlang Compiler == 5 | 6 | %% Erlang compiler options 7 | {erl_opts, [ warn_unused_vars 8 | , warn_export_all 9 | , warn_shadow_vars 10 | , warn_unused_import 11 | , warn_unused_function 12 | , warn_bif_clash 13 | , warn_unused_record 14 | , warn_deprecated_function 15 | , warn_obsolete_guard 16 | , strict_validation 17 | , warn_export_vars 18 | , warn_exported_vars 19 | , warn_missing_spec 20 | , warn_untyped_record 21 | , debug_info]}. 22 | 23 | {profiles, [ 24 | {test, [ 25 | {deps, [ {mixer, {git, "https://github.com/inaka/mixer.git", {tag, "0.1.5"}}} 26 | , {katana_test, "0.1.1"} 27 | ]} 28 | ]}, 29 | {shell, [ 30 | {deps, [ 31 | {sync, {git, "https://github.com/rustyio/sync.git", {ref, "9c78e7b"}}} 32 | ]} 33 | ]} 34 | ]}. 35 | 36 | %% == Common Test == 37 | 38 | {ct_compile_opts, [ warn_unused_vars 39 | , warn_export_all 40 | , warn_shadow_vars 41 | , warn_unused_import 42 | , warn_unused_function 43 | , warn_bif_clash 44 | , warn_unused_record 45 | , warn_deprecated_function 46 | , warn_obsolete_guard 47 | , strict_validation 48 | , warn_export_vars 49 | , warn_exported_vars 50 | , warn_missing_spec 51 | , warn_untyped_record 52 | , debug_info]}. 53 | 54 | {ct_opts, []}. 55 | 56 | %% == Cover == 57 | 58 | {cover_enabled, true}. 59 | 60 | {cover_opts, [verbose]}. 61 | 62 | %% == Dependencies == 63 | 64 | {deps, []}. 65 | 66 | %% == Dialyzer == 67 | 68 | {dialyzer, [ {warnings, [ no_return 69 | , unmatched_returns 70 | , error_handling 71 | ]} 72 | , {plt_apps, top_level_deps} 73 | , {plt_extra_apps, []} 74 | , {plt_location, local} 75 | , {base_plt_apps, [stdlib, kernel]} 76 | , {base_plt_location, global}]}. 77 | 78 | %% == Shell == 79 | 80 | {shell, [{apps, [sync]}]}. -------------------------------------------------------------------------------- /src/nconf.app.src: -------------------------------------------------------------------------------- 1 | { application 2 | , nconf 3 | , [ {description, "Nested Configuration Manager for Erlang Applications."} 4 | , {vsn, "0.0.1"} 5 | , {applications, 6 | [ kernel 7 | , stdlib 8 | ]} 9 | , {modules, []} 10 | , {mod, {nconf, []}} 11 | , {registered, []} 12 | , {maintainers,["Inaka"]} 13 | , {licenses,["Apache 2.0"]} 14 | , {links,[ {"Github", "https://github.com/inaka/nconf"}]} 15 | , {build_tools,["rebar3"]} 16 | ] 17 | }. 18 | -------------------------------------------------------------------------------- /src/nconf.erl: -------------------------------------------------------------------------------- 1 | %%%============================================================================= 2 | %%% @copyright (C) 2015, Erlang Solutions Ltd 3 | %%% @doc Module for reading the contents of an nconf config file and modifying 4 | %%% the configuration parameters accordingly. 5 | %%% 6 | %%% The following commands are available in an nconf config file: 7 | %%% 8 | %%% ``` 9 | %%% {set, AppName, ParamName, Path1, ..., PathN, Replacement} 10 | %%% {replace, AppName, ParamName, Path1, ..., PathN, Replacement} 11 | %%% {unset, AppName, ParamName, Path1, ..., PathN} 12 | %%% ''' 13 | %%% @end 14 | %%%============================================================================= 15 | -module(nconf). 16 | -copyright("2015, Erlang Solutions Ltd."). 17 | 18 | %% API 19 | -export([apply_config/1]). 20 | 21 | %% Types 22 | -export_type([config_tuples/0]). 23 | 24 | %% Exports for unit test 25 | -ifdef(TEST). 26 | %% Cover ignores the `export_all' option prior to R16B03, so export 27 | %% functions used in EUnit tests directly here 28 | -export([read_config/1, 29 | apply_config_tuples/1]). 30 | -endif. 31 | 32 | %%------------------------------------------------------------------------------ 33 | %% Types 34 | %%------------------------------------------------------------------------------ 35 | 36 | -type config_tuples() :: [tuple()]. 37 | %% A list of tuples, representing the contents of an nconf config file. 38 | 39 | %%%============================================================================= 40 | %%% External functions 41 | %%%============================================================================= 42 | 43 | %%------------------------------------------------------------------------------ 44 | %% @doc Modify the configuration parameters according to an nconf config file. 45 | %% @end 46 | %%------------------------------------------------------------------------------ 47 | -spec apply_config(FileName :: file:name_all()) -> any(). 48 | apply_config(FileName) -> 49 | case read_config(FileName) of 50 | {ok, ConfigTuples} -> 51 | case apply_config_tuples(ConfigTuples) of 52 | ok -> 53 | ok; 54 | {error, Errors} -> 55 | ApplyErrorMsg = "Error when applying ~s: ~p", 56 | [ error_logger:error_msg(ApplyErrorMsg, [FileName, Error]) 57 | || Error <- Errors 58 | ] 59 | end; 60 | {error, {_, ReasonStr}} -> 61 | ReadErrorMsg = "Error when reading ~s: ~s", 62 | error_logger:error_msg(ReadErrorMsg, [FileName, ReasonStr]) 63 | end. 64 | 65 | %%%============================================================================= 66 | %%% Internal functions 67 | %%%============================================================================= 68 | 69 | %%------------------------------------------------------------------------------ 70 | %% @doc Read an nconf config file. 71 | %% @end 72 | %%------------------------------------------------------------------------------ 73 | -spec read_config(FileName :: file:name_all()) -> 74 | {ok, config_tuples()} | 75 | {error, {Reason :: term(), ReasonStr :: string()}}. 76 | read_config(FileName) -> 77 | case file:consult(FileName) of 78 | {ok, Terms} -> 79 | {ok, Terms}; 80 | {error, Reason} -> 81 | ErrorStr = file:format_error(Reason), 82 | {error, {Reason, ErrorStr}} 83 | end. 84 | 85 | %%------------------------------------------------------------------------------ 86 | %% @doc Modify the configuration parameters according to the configuration 87 | %% tuples. 88 | %% @end 89 | %%------------------------------------------------------------------------------ 90 | -spec apply_config_tuples(Config :: config_tuples()) -> 91 | ok | 92 | {error, ErrorList :: [term()]}. % todo 93 | apply_config_tuples(Config) -> 94 | ErrorFun = 95 | fun 96 | (ConfigTuple) when is_tuple(ConfigTuple) -> 97 | case apply_config_tuple(tuple_to_list(ConfigTuple)) of 98 | ok -> 99 | %% Don't add anything to the error list. 100 | false; 101 | {error, Reason} -> 102 | %% Add the error to the error list. 103 | {true, {Reason, ConfigTuple}} 104 | end; 105 | (Term) -> 106 | %% Add the error to the error list. 107 | {true, {not_a_tuple, Term}} 108 | end, 109 | ErrorList = lists:filtermap(ErrorFun, Config), 110 | case ErrorList of 111 | [] -> 112 | ok; 113 | _ -> 114 | {error, ErrorList} 115 | end. 116 | 117 | %%------------------------------------------------------------------------------ 118 | %% @doc Modify the configuration parameters according to the configuration 119 | %% tuple. 120 | %% 121 | %% This function receives the tuple in a list so that it can do pattern matching 122 | %% on it. 123 | %% 124 | %% ConfigTuple formats: 125 | %% 126 | %% ``` 127 | %% {set, AppName, ParamName, Path1, ..., PathN, Replacement} 128 | %% {replace, AppName, ParamName, Path1, ..., PathN, Replacement} 129 | %% {unset, AppName, ParamName, Path1, ..., PathN} 130 | %% ''' 131 | %% @end 132 | %%------------------------------------------------------------------------------ 133 | -spec apply_config_tuple(ConfigTuple :: [term()]) -> ok | 134 | {error, Reason :: term()}. 135 | apply_config_tuple([set, AppName, ParamName | Rest = [_|_]]) -> 136 | Path = droplast(Rest), 137 | Replacement = {set, lists:last(Rest)}, 138 | case apply_config_change(AppName, ParamName, Path, Replacement) of 139 | ok -> 140 | ok; 141 | {error, _} = Error -> 142 | Error 143 | end; 144 | apply_config_tuple([replace, AppName, ParamName | Rest = [_, _|_]]) -> 145 | %% A 'replace' command needs to have at least one Path component, since the 146 | %% AppName/ParamName itself cannot be replaced with a custom term. 147 | Path = droplast(Rest), 148 | Replacement = {replace, lists:last(Rest)}, 149 | case apply_config_change(AppName, ParamName, Path, Replacement) of 150 | ok -> 151 | ok; 152 | {error, _} = Error -> 153 | Error 154 | end; 155 | apply_config_tuple([unset, AppName, ParamName | Path]) -> 156 | case apply_config_change(AppName, ParamName, Path, unset) of 157 | ok -> 158 | ok; 159 | {error, _} = Error -> 160 | Error 161 | end; 162 | apply_config_tuple([Cmd|_]) when Cmd =:= set; 163 | Cmd =:= unset; 164 | Cmd =:= replace -> 165 | {error, tuple_too_short}; 166 | apply_config_tuple([Cmd|_]) -> 167 | {error, {unknown_command, Cmd}}. 168 | 169 | %%------------------------------------------------------------------------------ 170 | %% @doc Change the value of the configuration entry 171 | %% AppName/ParamName/Path1/.../PathN to `Replacement'. 172 | %% @end 173 | %%------------------------------------------------------------------------------ 174 | -spec apply_config_change(AppName :: atom(), 175 | ParamName :: atom(), 176 | Path :: [term()], 177 | Replacement :: {set, term()} | 178 | {replace, term()} | 179 | unset) -> 180 | ok | 181 | {error, Reason :: term()}. 182 | apply_config_change(AppName, ParamName, [], unset) -> 183 | %% Unset the whole config entry, e.g.: 184 | %% 185 | %% {unset, snmp, agent}. 186 | application:unset_env(AppName, ParamName); 187 | apply_config_change(AppName, ParamName, Path, Replacement) -> 188 | OldValue = 189 | case application:get_env(AppName, ParamName) of 190 | undefined -> 191 | %% We assume that this parameter is a proplist so that 192 | %% replace/3 can dig into it. If it is fully replaced, that's 193 | %% fine too. 194 | []; 195 | {ok, Val} -> 196 | Val 197 | end, 198 | case replace(OldValue, Path, Replacement) of 199 | {ok, NewValue} -> 200 | application:set_env(AppName, ParamName, NewValue), 201 | ok; 202 | {error, _} = Error -> 203 | Error 204 | end. 205 | 206 | %%------------------------------------------------------------------------------ 207 | %% @doc Replace the term inside `OldValue' that can be accessed through the 208 | %% given path with `Replacement'. 209 | %% 210 | %% Examples: 211 | %% 212 | %% ``` 213 | %% - replace(old, [], new) -> new 214 | %% - replace([{a, [{b, 1}, {c, 2}]}, {d, 3}], [a, b], 0) -> 215 | %% [{a, [{b, 0}, {c, 2}]}, {d, 3}] 216 | %% ''' 217 | %% @end 218 | %%------------------------------------------------------------------------------ 219 | -spec replace(OldValue :: term(), 220 | Path :: [term()], 221 | Replacement :: {set, term()} | 222 | {replace, term()} | 223 | unset) -> 224 | {ok, NewValue :: term()} | 225 | {error, {tuple_list_expected, Keys :: [term()], OldValue2 :: term()}}. 226 | 227 | replace(_OldValue, [] = _Path, {set, ReplacementTerm}) -> 228 | %% We need to simply replace the old value with the replacement. 229 | %% Example: 230 | %% - OldValue = 1 231 | %% - Path = [] 232 | %% - ReplacementTerm = 2 233 | %% --> Result = 2 234 | 235 | {ok, ReplacementTerm}; 236 | 237 | replace(OldTupleList, [Key] = _Path, {replace, ReplacementTuple}) 238 | when is_list(OldTupleList) -> 239 | %% Key is present in the OldTupleList, and there is no more element 240 | %% in the Path, so we should replace the key in OldTupleList. 241 | %% Example: 242 | %% - OldTupleList = [{a, [{x, 1}, {y, 2}]}, {b, 2}] 243 | %% - Path = [a] 244 | %% - Replacement = {a, 1, []} 245 | %% --> Result = [{a, 1, []}, {b, 2}] 246 | 247 | {ok, [ReplacementTuple|lists:keydelete(Key, 1, OldTupleList)]}; 248 | 249 | replace(OldTupleList, [Key] = _Path, unset) when is_list(OldTupleList) -> 250 | %% Key is present in the OldTupleList, and there is no more element 251 | %% in the Path, so we should remove the key from OldTupleList. 252 | %% Example 1: 253 | %% - OldTupleList = [{a, "", 1}, {b, 2}] 254 | %% - Path = [a] 255 | %% --> Result = [{b, 2}] 256 | 257 | {ok, lists:keydelete(Key, 1, OldTupleList)}; 258 | 259 | replace(OldTupleList, [Key|PathRest] = Path, Replacement) 260 | when is_list(OldTupleList) -> 261 | case {Replacement, lists:keyfind(Key, 1, OldTupleList)} of 262 | {unset, false} -> 263 | %% Key is not present and we want to delete it, so there is nothing 264 | %% to be done. 265 | %% Example: 266 | %% - OldTupleList = [{a, [{x, 1}, {y, 2}]}, {b, 2}] 267 | %% - Path = [c, x] 268 | %% --> Result = [{a, [{x, 1}, {y, 2}]}, {b, 2}] 269 | {ok, OldTupleList}; 270 | {{set, ReplacementTerm}, false} -> 271 | %% Key is not present in the OldTupleList, so it should be added. 272 | %% Example 1: 273 | %% - OldTupleList = [{a, 1}, {b, 2}] 274 | %% - Path = [c] 275 | %% - ReplacementTerm: 33 276 | %% --> Result = [{c, 33}, {a, 1}, {b, 2}] 277 | %% Example 2: 278 | %% - OldTupleList = [{a, [{x, 1}, {y, 2}]}, {b, 2}] 279 | %% - Path = [c, x] 280 | %% - ReplacementTerm = 33 281 | %% --> Result = [{c, [{x, 33}]}, {a, [{x, 1}, {y, 2}]}, {b, 2}] 282 | %% 283 | %% Coming into this branch might be the result of a typo in 284 | %% the config, so let's log it. 285 | InfoMsg = 286 | "Configuring non-configured path: ~p " 287 | "(OldTupleList=~p, ReplacementTerm=~p)", 288 | error_logger:info_msg(InfoMsg, 289 | [Path, OldTupleList, ReplacementTerm]), 290 | 291 | NewElement = 292 | lists:foldl( 293 | %% Example 2 execution: 294 | %% 1. Key = x, Acc = 33 --> [{x, 33}] 295 | %% 2. Key = c, Acc = [{x, 33}] --> [{c, [{x, 33}]}] 296 | fun(Key0, Acc) -> 297 | [{Key0, Acc}] 298 | end, ReplacementTerm, lists:reverse(Path)), 299 | {ok, NewElement ++ OldTupleList}; 300 | 301 | {{replace, ReplacementTuple}, false} -> 302 | %% Key is not present in the OldTupleList, so it should be added. 303 | 304 | %% Example 1: 305 | %% - OldTupleList = [{a, 1}, {b, 2}] 306 | %% - Path = [c] 307 | %% - ReplacementTuple: {c, 1, 2} 308 | %% --> Result = [{c, 1, 2}, {a, 1}, {b, 2}] 309 | %% 310 | %% Example 2: 311 | %% - OldTupleList = [{a, [{x, 1}, {y, 2}]}, {b, 2}] 312 | %% - Path = [c, x] 313 | %% - ReplacementTuple = {x, 1, 2} 314 | %% --> Result = [{c, [{x, 1, 2}]}, {a, [{x, 1}, {y, 2}]}, {b, 2}] 315 | 316 | %% Coming into this branch might be the result of a typo in 317 | %% the config, so let's log it. 318 | InfoMsg = 319 | "Configuring non-configured path: ~p " 320 | "(OldTupleList=~p, ReplacementTuple=~p)", 321 | error_logger:info_msg(InfoMsg, 322 | [Path, OldTupleList, ReplacementTuple]), 323 | 324 | Path2 = droplast(Path), 325 | NewElement = 326 | lists:foldl( 327 | %% Example 2 execution: 328 | %% 1. Key = c, Acc = {x, 1, 2} --> {c, [{x, 1, 2}]} 329 | fun(Key0, Acc) -> 330 | {Key0, [Acc]} 331 | end, ReplacementTuple, lists:reverse(Path2)), 332 | {ok, [NewElement|OldTupleList]}; 333 | 334 | {_Replacement, OldTuple} -> 335 | %% Key is present in the OldTupleList, so we should continue 336 | %% following the Path. 337 | %% Example 1: 338 | %% - OldTupleList = [{a, "", 1}, {b, 2}] 339 | %% - Path = [a] 340 | %% - Replacement = {set, 11} 341 | %% - OldTuple = {a, "", 1} 342 | %% --> Result = [{a, "", 11}, {b, 2}] 343 | %% Example 2: 344 | %% - OldTupleList = [{a, [{x, 1}, {y, 2}]}, {b, 2}] 345 | %% - Path = [a, x] 346 | %% - Replacement = {set, 11} 347 | %% - OldTuple = {a, [{x, 1}, {y, 2}]} 348 | %% --> Result = [{a, [{x, 11}, {y, 2}]}, {b, 2}] 349 | %% Example 3: 350 | %% - OldTupleList = [{a, [{x, 1}, {y, 2}]}, {b, 2}] 351 | %% - Path = [a, x] 352 | %% - Replacement = unset 353 | %% - OldTuple = {a, [{x, 1}, {y, 2}]} 354 | %% --> Result = [{a, [{y, 2}]}, {b, 2}] 355 | %% Example 4: 356 | %% - OldTupleList = [{a, [{x, 1}, {y, 2}]}, {b, 2}] 357 | %% - Path = [a, x] 358 | %% - Replacement = {replace, {x, 1, 2}} 359 | %% - OldTuple = {a, [{x, 1}, {y, 2}]} 360 | %% --> Result = [{a, [{x, 1, 2}]}, {b, 2}] 361 | %% 362 | %% We don't assume that the tuple is a pair, because we might have 363 | %% let's say tuples of 4: 364 | %% 365 | %% {main_key, 366 | %% [one], 367 | %% [two], 368 | %% [{key, value}]} 369 | %% 370 | %% With the current implementation, the following will work in the 371 | %% nconf config: 372 | %% 373 | %% {..., 374 | %% main_key, 375 | %% key, 376 | %% value} 377 | %% 378 | %% Therefore when we have a tuple (like the 4-tuple above), we 379 | %% should follow the last element of the tuple. 380 | LastElement = element(size(OldTuple), OldTuple), 381 | case replace(LastElement, PathRest, Replacement) of 382 | {ok, NewLastElement} -> 383 | NewTuple = 384 | setelement(size(OldTuple), OldTuple, NewLastElement), 385 | NewTupleList = 386 | lists:keyreplace(Key, 1, OldTupleList, NewTuple), 387 | {ok, NewTupleList}; 388 | {error, {tuple_list_expected, Keys, OldValue}} -> 389 | {error, {tuple_list_expected, [Key|Keys], OldValue}} 390 | end 391 | end; 392 | 393 | replace(OldValue, [Key|_], _Replacement) -> 394 | {error, {tuple_list_expected, [Key], OldValue}}. 395 | 396 | %%------------------------------------------------------------------------------ 397 | %% @doc Return the list dropping its last element. 398 | %% 399 | %% Copied from lists:droplast, which was added to OTP in 17.0. 400 | %% @end 401 | %%------------------------------------------------------------------------------ 402 | -spec droplast(List :: [T, ...]) -> [T] when T :: term(). 403 | droplast([_T]) -> []; 404 | droplast([H | T]) -> [H | droplast(T)]. 405 | -------------------------------------------------------------------------------- /test/nconf_meta_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(nconf_meta_SUITE). 2 | 3 | -include_lib("mixer/include/mixer.hrl"). 4 | -mixin([{ktn_meta_SUITE 5 | , [ all/0 6 | , xref/1 7 | , dialyzer/1 8 | , elvis/1 9 | ] 10 | }]). 11 | 12 | -export([init_per_suite/1]). 13 | 14 | -type config() :: [{atom(), term()}]. 15 | 16 | -spec init_per_suite(config()) -> config(). 17 | init_per_suite(Config) -> [{application, nconf} | Config]. 18 | -------------------------------------------------------------------------------- /test/nconf_test.erl: -------------------------------------------------------------------------------- 1 | %%%============================================================================= 2 | %%% @copyright 2015, Erlang Solutions Ltd 3 | %%% @doc Unit test 4 | %%% @end 5 | %%%============================================================================= 6 | -module(nconf_test). 7 | -copyright("2015, Erlang Solutions Ltd."). 8 | 9 | -ifdef(TEST). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | %% Test file used during the tests. 14 | -define(TEST_FILE, "nconf_config_test_file.nconf"). 15 | -define(TEST_FILE_CONTENTS, <<"{a, 1}.\n{b, 2}.">>). 16 | 17 | -define(assertEqualSorted(A, B), ?assertEqual(lists:sort(A), lists:sort(B))). 18 | 19 | %%%============================================================================= 20 | %%% Test functions 21 | %%%============================================================================= 22 | 23 | self_test() -> 24 | %% We don't have any config params when started. 25 | ?assertEqualSorted([], get_all_env()), 26 | 27 | %% Setting non-empty config params. 28 | _ = set_all_env([{a, 1}, {b, 2}]), 29 | ?assertEqualSorted([{a, 1}, {b, 2}], get_all_env()), 30 | 31 | %% Clearing the config params. 32 | _ = set_all_env([]), 33 | ?assertEqualSorted([], get_all_env()). 34 | 35 | read_config_test() -> 36 | 37 | %% Basic test 38 | _ = file:delete(?TEST_FILE), 39 | ok = file:write_file(?TEST_FILE, ?TEST_FILE_CONTENTS), 40 | ?assertEqual({ok, [{a, 1}, {b, 2}]}, nconf:read_config(?TEST_FILE)), 41 | 42 | %% Non-existing file 43 | _ = file:delete(?TEST_FILE), 44 | Expected1 = {error, {enoent, "no such file or directory"}}, 45 | ?assertEqual(Expected1, nconf:read_config(?TEST_FILE)), 46 | 47 | %% Bad file according to file:consult 48 | _ = file:delete(?TEST_FILE), 49 | ok = file:write_file(?TEST_FILE, <<"not erlang terms">>), 50 | {error, {ObtainedReason, ReasonStr}} = nconf:read_config(?TEST_FILE), 51 | Expected2 = {1, erl_parse, ["syntax error before: ", "terms"]}, 52 | Expected3 = <<"1: syntax error before: terms">>, 53 | ?assertEqual(Expected2, ObtainedReason), 54 | ?assertEqual(Expected3, unicode:characters_to_binary(ReasonStr)). 55 | 56 | apply_config_tuples_test() -> 57 | 58 | %% 01 No config tuple 59 | _ = set_all_env([]), 60 | ?assertEqual(ok, nconf:apply_config_tuples([])), 61 | ?assertEqualSorted([], get_all_env()), 62 | 63 | %% 02 Setting full config params (i.e. we use 3 long config tuples) 64 | _ = set_all_env([{existing_param_a, 1}, {existing_param_b, 2}]), 65 | NConf02 = 66 | [ {set, my_test_app, existing_param_a, 11} 67 | , {set, my_test_app, new_param, 33} 68 | ], 69 | ?assertEqual(ok, nconf:apply_config_tuples(NConf02)), 70 | 71 | Expected02 = 72 | [ {existing_param_a, 11} 73 | , {existing_param_b, 2} 74 | , {new_param, 33} 75 | ], 76 | ?assertEqualSorted(Expected02, get_all_env()), 77 | 78 | %% 03 Deleting full config params (i.e. we use 2 long config tuples) 79 | _ = set_all_env([{existing_param_a, 1}, {existing_param_b, 2}]), 80 | 81 | NConf03 = 82 | [ {unset, my_test_app, existing_param_a} 83 | , {unset, my_test_app, new_param} 84 | ], 85 | ?assertEqual(ok, nconf:apply_config_tuples(NConf03)), 86 | 87 | Expected03 = [{existing_param_b, 2}], 88 | ?assertEqualSorted(Expected03, get_all_env()), 89 | 90 | %% 04 Setting deep config params: Update existing values 91 | Env04 = 92 | [ { existing_param_a 93 | , [ { x, 1 } 94 | , { y, 2 } 95 | ] } 96 | , { existing_param_b 97 | , [ { x1, 1 } 98 | , { x 99 | , [ { y 100 | , [ { z, old } 101 | , { z2, 2 } 102 | ] 103 | } 104 | , { y2, 3 } 105 | ] } ] } ], 106 | _ = set_all_env(Env04), 107 | 108 | NConf04 = 109 | [ { set, my_test_app, existing_param_a, x, 11 } 110 | , { set, my_test_app, existing_param_a, z, 33 } 111 | , { set, my_test_app, existing_param_b, x, y, z, new } 112 | , { set, my_test_app, existing_param_b, x, y2, new2 } 113 | ], 114 | ?assertEqual(ok, nconf:apply_config_tuples(NConf04)), 115 | 116 | Expected04 = 117 | [ { existing_param_a 118 | , [ { z, 33 } 119 | , { x, 11 } 120 | , { y, 2 } 121 | ] } 122 | , { existing_param_b 123 | , [ { x1, 1 } 124 | , { x 125 | , [ { y 126 | , [ { z, new } 127 | , { z2, 2 } 128 | ] } 129 | , { y2, new2 } 130 | ] } ] } ], 131 | ?assertEqualSorted(Expected04, get_all_env()), 132 | 133 | %% 05 Replacing deep config params: Update existing values 134 | Env05 = 135 | [ { existing_param_a 136 | , [ { x, 1 } 137 | , { y, 2 } 138 | ] } 139 | , { existing_param_b 140 | , [ { x1, 1 } 141 | , { x 142 | , [ { y 143 | , [ { z, old } 144 | , { z2, 2 } 145 | ] } 146 | , { y2, 3 } 147 | ] } ] } ], 148 | _ = set_all_env(Env05), 149 | 150 | NConf05 = 151 | [ { replace, my_test_app, existing_param_a, x, { x, 1, 1 } } 152 | , { replace, my_test_app, existing_param_a, z, { z, 3, 3 } } 153 | , { replace, my_test_app, existing_param_b, x, y, z, { z, new, new } } 154 | , { replace, my_test_app, existing_param_b, x, y2, { y2, new2, new2 } } 155 | ], 156 | ?assertEqual(ok, nconf:apply_config_tuples(NConf05)), 157 | 158 | Expected05 = 159 | [ { existing_param_a 160 | , [ { z, 3, 3 } 161 | , { x, 1, 1 } 162 | , { y, 2 } 163 | ] } 164 | , { existing_param_b 165 | , [ { x1, 1 } 166 | , { x 167 | , [ { y2, new2, new2 } 168 | , { y 169 | , [ { z, new, new } 170 | , { z2, 2 } 171 | ] } ] } ] } ], 172 | ?assertEqualSorted(Expected05, get_all_env()), 173 | 174 | %% 06 Deleting deep config params: Deleting existing values 175 | Env06 = 176 | [ { existing_param_a 177 | , [ { x, 1 } 178 | , { y, 2 } 179 | ] } 180 | , { existing_param_b 181 | , [ { x1, 1 } 182 | , { x 183 | , [ { y 184 | , [ { z, old } 185 | , { z2, 2 } 186 | ] } 187 | , { y2, 3 } 188 | ] } ] } ], 189 | _ = set_all_env(Env06), 190 | 191 | NConf06 = 192 | [ { unset, my_test_app, existing_param_a, x } 193 | , { unset, my_test_app, existing_param_a, z } 194 | , { unset, my_test_app, existing_param_b, x, y, z } 195 | , { unset, my_test_app, existing_param_b, x, y2 } 196 | ], 197 | ?assertEqual(ok, nconf:apply_config_tuples(NConf06)), 198 | 199 | Expected06 = 200 | [ { existing_param_a, [{y, 2}] } 201 | , { existing_param_b 202 | , [ { x1, 1 } 203 | , { x 204 | , [ { y 205 | , [ { z2, 2 } 206 | ] } ] } ] } ], 207 | ?assertEqualSorted(Expected06, get_all_env()), 208 | 209 | %% 07 Setting deep config params: Add new values to existing params 210 | Env07 = 211 | [ { existing_param_a 212 | , [ { x, 1 } 213 | , { y, 2 } 214 | ] } 215 | , { existing_param_b 216 | , [ { x1, 1 } 217 | , { x 218 | , [ { y 219 | , [ { z, old } 220 | , { z2, 2 } 221 | ] } 222 | , { y2, 3 } 223 | ] } ] } ], 224 | _ = set_all_env(Env07), 225 | 226 | NConf07 = 227 | [ { set, my_test_app, existing_param_b, x_new, 0 } 228 | , { set, my_test_app, existing_param_b, x, y_new, 0 } 229 | , { set, my_test_app, existing_param_b, x, y, z_new3, 0 } 230 | ], 231 | ?assertEqual(ok, nconf:apply_config_tuples(NConf07)), 232 | 233 | Expected07 = 234 | [ { existing_param_a 235 | , [ { x, 1 } 236 | , { y, 2 } 237 | ] } 238 | , { existing_param_b 239 | , [ { x_new, 0 } 240 | , { x1, 1 } 241 | , { x 242 | , [ { y_new, 0 } 243 | , { y 244 | , [{z_new3, 0}, {z, old}, {z2, 2}]}, {y2, 3}]}]}], 245 | ?assertEqualSorted(Expected07, get_all_env()), 246 | 247 | %% 08 Replacing deep config params: Add new values to existing params 248 | Env08 = 249 | [ { existing_param_a 250 | , [ { x, 1 } 251 | , { y, 2 } 252 | ] } 253 | , { existing_param_b 254 | , [ { x1, 1 } 255 | , { x 256 | , [ { y 257 | , [ { z, old } 258 | , { z2, 2 } 259 | ] } 260 | , { y2, 3 } 261 | ] } ] } ], 262 | _ = set_all_env(Env08), 263 | 264 | NConf08 = 265 | [ { replace, my_test_app, existing_param_b, x_new, { x_new, 0 } } 266 | , { replace, my_test_app, existing_param_b, x, y_new, { y_new, 0 } } 267 | , { replace, my_test_app, existing_param_b, x, y, z_new3, { z_new3, 0 } } 268 | ], 269 | ?assertEqual(ok, nconf:apply_config_tuples(NConf08)), 270 | 271 | Expected08 = 272 | [ { existing_param_a 273 | , [ { x, 1 } 274 | , { y, 2 } 275 | ] } 276 | , { existing_param_b 277 | , [ { x_new, 0 } 278 | , { x1, 1 } 279 | , { x 280 | , [ { y_new, 0 } 281 | , { y 282 | , [ { z_new3, 0 } 283 | , { z, old } 284 | , { z2, 2 } 285 | ] } 286 | , { y2, 3 } 287 | ] } ] } ], 288 | ?assertEqualSorted(Expected08, get_all_env()), 289 | 290 | %% 09 Setting deep config params: Add new values to new params 291 | _ = set_all_env([{existing_param_a, [{x, 1}, {y, 2}]}]), 292 | NConf09 = 293 | [ { set, my_test_app, new_param_a, a, 1 } 294 | , { set, my_test_app, new_param_b, b, bb, 2 } 295 | , { set, my_test_app, new_param_c, c, cc, ccc, 3 } 296 | ], 297 | ?assertEqual(ok, nconf:apply_config_tuples(NConf09)), 298 | 299 | Expected09 = 300 | [ { existing_param_a 301 | , [ { x, 1 } 302 | , { y, 2 } 303 | ] } 304 | , { new_param_a 305 | , [ { a, 1 } 306 | ] } 307 | , { new_param_b 308 | , [ { b 309 | , [ { bb, 2 } 310 | ] } ] } 311 | , { new_param_c 312 | , [ { c 313 | , [ { cc 314 | , [ { ccc, 3 } 315 | ] } ] } ] } ], 316 | ?assertEqualSorted(Expected09, get_all_env()), 317 | 318 | %% 10 Replacing deep config params: Add new values to new params 319 | _ = set_all_env([{existing_param_a, [{x, 1}, {y, 2}]}]), 320 | NConf10 = 321 | [ { replace, my_test_app, new_param_a, a, { a, 1 } } 322 | , { replace, my_test_app, new_param_b, b, bb, { bb, 2 } } 323 | , { replace, my_test_app, new_param_c, c, cc, ccc, { ccc, 3 } } 324 | ], 325 | ?assertEqual(ok, nconf:apply_config_tuples(NConf10)), 326 | Expected10 = 327 | [ { existing_param_a 328 | , [ { x, 1 } 329 | , { y, 2 } 330 | ] } 331 | , { new_param_a 332 | , [ { a, 1 } 333 | ] } 334 | , { new_param_b 335 | , [ { b 336 | , [ { bb, 2 } 337 | ] } ] } 338 | , { new_param_c 339 | , [ { c 340 | , [ { cc 341 | , [ { ccc, 3 } 342 | ] } ] } ] } ], 343 | ?assertEqualSorted(Expected10, get_all_env()), 344 | 345 | %% 11 Test errors; and test the fact that if there is an error, other 346 | %% settings are still performed. 347 | _ = set_all_env([{existing_param_a, [{x, 1}, {y, 2}]}]), 348 | Error11 = 349 | { error 350 | , [ { { tuple_list_expected 351 | , [ y, y2 ] 352 | , 2 353 | } 354 | , { set, my_test_app, existing_param_a, y, y2, 2 } 355 | } ] }, 356 | NConf11 = 357 | [ { set, my_test_app, existing_param_a, y, y2, 2 } 358 | , { set, my_test_app, existing_param_a, x, 11 } 359 | ], 360 | ?assertEqual(Error11, nconf:apply_config_tuples(NConf11)), 361 | Expected11 = [ { existing_param_a, [ {x, 11 }, { y, 2 } ] } ], 362 | ?assertEqualSorted(Expected11, get_all_env()), 363 | 364 | %% 12 365 | _ = set_all_env([ { existing_param_a, [ { x, 1 }, { y, 2 } ] } ] ), 366 | Error12 = 367 | { error 368 | , [ { not_a_tuple, [something] } 369 | , { tuple_too_short, { set, my_test_app, param } } 370 | , { tuple_too_short, { unset, my_test_app } } 371 | , { { unknown_command, mycmd }, { mycmd, x } } 372 | ] 373 | }, 374 | NConf12 = 375 | [ [ something ] 376 | , { set, my_test_app, existing_param_a, x, 11 } 377 | , { set, my_test_app, param } 378 | , { unset, my_test_app } 379 | , { mycmd, x } 380 | ], 381 | ?assertEqual(Error12, nconf:apply_config_tuples(NConf12)), 382 | Expected12 = [ { existing_param_a, [ { x, 11 }, { y, 2 } ] } ], 383 | ?assertEqualSorted(Expected12, get_all_env()), 384 | 385 | %% 13 A 'replace' command needs to have at least one Path component, since 386 | %% the AppName/ParamName itself cannot be replaced with a custom term. 387 | _ = set_all_env([ { existing_param_a, [ { x, 1 }, { y, 2 } ] } ] ), 388 | 389 | Error13 = 390 | { error 391 | , [ { tuple_too_short 392 | , { replace, my_test_app, existing_param_a, { existing_param_a, 2 } } 393 | } ] }, 394 | NConf13 = 395 | [ { replace, my_test_app, existing_param_a, { existing_param_a, 2 } } ], 396 | ?assertEqual(Error13, nconf:apply_config_tuples(NConf13)), 397 | Expected13 = [ { existing_param_a, [ { x, 1 }, { y, 2 } ] } ], 398 | ?assertEqualSorted(Expected13, get_all_env()), 399 | ok. 400 | 401 | %%%============================================================================= 402 | %%% Internal functions 403 | %%%============================================================================= 404 | 405 | %%------------------------------------------------------------------------------ 406 | %% @doc Get the list of config parameters for the my_test_app application. 407 | %% @end 408 | %%------------------------------------------------------------------------------ 409 | -spec get_all_env() -> [{Par :: atom(), Val :: term()}]. 410 | get_all_env() -> 411 | application:get_all_env(my_test_app). 412 | 413 | %%------------------------------------------------------------------------------ 414 | %% @doc Set the list of config parameters for the my_test_app application. 415 | %% @end 416 | %%------------------------------------------------------------------------------ 417 | -spec set_all_env([{Par :: atom(), Val :: term()}]) -> any(). 418 | set_all_env(NewConfigParams) -> 419 | %% Delete all existing config params. 420 | [ application:unset_env(my_test_app, Par) 421 | || {Par, _Val} <- application:get_all_env(my_test_app) 422 | ], 423 | 424 | %% Set the new config params. 425 | [ application:set_env(my_test_app, Par, Val) 426 | || {Par, Val} <- NewConfigParams 427 | ]. 428 | 429 | -endif. 430 | --------------------------------------------------------------------------------