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 }