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