1 /*******************************************************************************
2
3 CLI args support for overriding config values.
4
5 Copyright:
6 Copyright (c) 2018 dunnhumby Germany GmbH.
7 All rights reserved.
8
9 License:
10 Boost Software License Version 1.0. See LICENSE_BOOST.txt for details.
11 Alternatively, this file may be distributed under the terms of the Tango
12 3-Clause BSD License (see LICENSE_BSD.txt for details).
13
14 *******************************************************************************/
15
16 module ocean.application.components.ConfigOverrides;
17
18 import ocean.transition;
19 import ocean.core.Verify;
20 import ocean.text.Arguments;
21 import ocean.text.Util : join, locate, locatePrior, trim;
22 import ocean.util.config.ConfigParser;
23
24 /*******************************************************************************
25
26 Setup args for overriding config options
27
28 Params:
29 args = parsed command line arguments
30
31 *******************************************************************************/
32
33 public void setupArgs ( Arguments args )
34 {
35 args("override-config").aliased('O').params(1,int.max).smush()
36 .help("override a configuration value "
37 ~ "(example: -O 'category.key = value', need a space between "
38 ~ "-O and the option now because of a Tango bug)");
39 }
40
41 /*******************************************************************************
42
43 Do a simple validation over override-config arguments
44
45 Params:
46 args = parsed command line arguments
47
48 Returns:
49 error text if any
50
51 *******************************************************************************/
52
53 public cstring validateArgs ( Arguments args )
54 {
55 istring[] errors;
56 foreach (opt; args("override-config").assigned)
57 {
58 istring cat, key, val;
59
60 auto error = parseArg(opt, cat, key, val);
61
62 if (!error.length)
63 continue;
64
65 errors ~= error;
66 }
67
68 auto ret = join(errors, ", ");
69 return assumeUnique(ret);
70 }
71
72 /*******************************************************************************
73
74 Process overridden config options
75
76 Params:
77 args = parsed command line arguments
78
79 *******************************************************************************/
80
81 public void handleArgs ( Arguments args, ConfigParser config )
82 {
83 istring category, key, value;
84
85 foreach (opt; args("override-config").assigned)
86 {
87 auto error = parseArg(opt, category, key, value);
88 verify (error is null,
89 "Unexpected error while processing overrides, errors " ~
90 "should have been caught by the validateArgs() method");
91 verify (config.exists(category, key),
92 "Attempt to override non-existent config entry"
93 );
94
95 if ( !value.length )
96 {
97 config.remove(category, key);
98 }
99 else
100 {
101 config.set(category, key, value);
102 }
103 }
104 }
105
106 /*******************************************************************************
107
108 Parses an overridden config.
109
110 Category, key and value are filled with slices to the original opt
111 string, so if you need to store them you probably want to dup() them.
112 No allocations are performed for those variables, although some
113 allocations are performed in case of errors (but only on errors).
114
115 Since keys can't have a dot ("."), cate.gory.key=value will be
116 interpreted as category="cate.gory", key="key" and value="value".
117
118 Params:
119 opt = the overridden config as specified on the command-line
120 category = buffer to be filled with the parsed category
121 key = buffer to be filled with the parsed key
122 value = buffer to be filled with the parsed value
123
124 Returns:
125 null if parsing was successful, an error message if there was an
126 error.
127
128 *******************************************************************************/
129
130 private istring parseArg ( istring opt, out istring category,
131 out istring key, out istring value )
132 {
133 opt = trim(opt);
134
135 if (opt.length == 0)
136 return "Option can't be empty";
137
138 auto key_end = locate(opt, '=');
139 if (key_end == opt.length)
140 return "No value separator ('=') found for config option " ~
141 "override: " ~ opt;
142
143 value = trim(opt[key_end + 1 .. $]);
144
145 auto cat_key = opt[0 .. key_end];
146 auto category_end = locatePrior(cat_key, '.');
147 if (category_end == cat_key.length)
148 return "No category separator ('.') found before the value " ~
149 "separator ('=') for config option override: " ~ opt;
150
151 category = trim(cat_key[0 .. category_end]);
152 if (category.length == 0)
153 return "Empty category for config option override: " ~ opt;
154
155 key = trim(cat_key[category_end + 1 .. $]);
156 if (key.length == 0)
157 return "Empty key for config option override: " ~ opt;
158
159 return null;
160 }
161
162 version (UnitTest)
163 {
164 import ocean.core.Test : NamedTest;
165 import ocean.core.Array : startsWith;
166 }
167
168 unittest
169 {
170 // Errors are compared only with startsWith(), not the whole error
171 void testParser ( istring opt, istring exp_cat, istring exp_key,
172 istring exp_val, istring expected_error = null )
173 {
174 istring cat, key, val;
175
176 auto t = new NamedTest(opt);
177
178 auto error = parseArg(opt, cat, key, val);
179
180 if (expected_error is null)
181 {
182 t.test(error is null, "Error message mismatch, expected no " ~
183 "error, got '" ~ error ~ "'");
184 }
185 else
186 {
187 t.test(error.startsWith(expected_error), "Error message " ~
188 "mismatch, expected an error starting with '" ~
189 expected_error ~ "', got '" ~ error ~ "'");
190 }
191
192 if (exp_cat is null && exp_key is null && exp_val is null)
193 return;
194
195 t.test!("==")(cat, exp_cat);
196 t.test!("==")(key, exp_key);
197 t.test!("==")(val, exp_val);
198 }
199
200 // Shortcut to test expected errors
201 void testParserError ( istring opt, istring expected_error )
202 {
203 testParser(opt, null, null, null, expected_error);
204 }
205
206 // New format
207 testParser("cat.key=value", "cat", "key", "value");
208 testParser("cat.key = value", "cat", "key", "value");
209 testParser("cat.key= value", "cat", "key", "value");
210 testParser("cat.key =value", "cat", "key", "value");
211 testParser("cat.key = value ", "cat", "key", "value");
212 testParser(" cat.key = value ", "cat", "key", "value");
213 testParser(" cat . key = value \t ", "cat", "key", "value");
214 testParser(" empty . val = \t ", "empty", "val", "");
215
216 // New format errors
217 testParserError("cat.key value", "No value separator ");
218 testParserError("key = value", "No category separator ");
219 testParserError("cat key value", "No value separator ");
220 testParserError(" . empty = cat\t ", "Empty category ");
221 testParserError(" empty . = key\t ", "Empty key ");
222 testParserError(" empty . val = \t ", null);
223 testParserError(" . = \t ", "Empty ");
224 }