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 }