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 }