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 }