1 /******************************************************************************* 2 3 Provide the suggested default configuration for applications 4 5 This module provide the basic tool to quickly get configuration parsing with 6 environment and command-line overrides. It assumes a YAML configuration. 7 8 Note: 9 This module name is inspired inspired by cURL's 'easy' API. 10 11 *******************************************************************************/ 12 13 module dub.internal.configy.easy; 14 15 public import dub.internal.configy.attributes; 16 public import dub.internal.configy.exceptions : ConfigException; 17 public import dub.internal.configy.read; 18 19 import std.getopt; 20 import std.typecons : Nullable, nullable; 21 22 /// Command-line arguments 23 public struct CLIArgs 24 { 25 /// Path to the config file 26 public string config_path = "config.yaml"; 27 28 /// Overrides for config options 29 public string[][string] overrides; 30 31 /// Helper to add items to `overrides` 32 public void overridesHandler (string, string value) 33 { 34 import std.string; 35 const idx = value.indexOf('='); 36 if (idx < 0) return; 37 string k = value[0 .. idx], v = value[idx + 1 .. $]; 38 if (auto val = k in this.overrides) 39 (*val) ~= v; 40 else 41 this.overrides[k] = [ v ]; 42 } 43 44 /*************************************************************************** 45 46 Parses the base command line arguments 47 48 This can be composed with the program argument. 49 For example, consider a program which wants to expose a `--version` 50 switch, the definition could look like this: 51 --- 52 public struct ProgramCLIArgs 53 { 54 public CLIArgs base; // This struct 55 56 public alias base this; // For convenience 57 58 public bool version_; // Program-specific part 59 } 60 --- 61 Then, an application-specific configuration routine would be: 62 --- 63 public GetoptResult parse (ref ProgramCLIArgs clargs, ref string[] args) 64 { 65 auto r = clargs.base.parse(args); 66 if (r.helpWanted) return r; 67 return getopt( 68 args, 69 "version", "Print the application version, &clargs.version_"); 70 } 71 --- 72 73 Params: 74 args = The command line args to parse (parsed options will be removed) 75 passThrough = Whether to enable `config.passThrough` and 76 `config.keepEndOfOptions`. `true` by default, to allow 77 composability. If your program doesn't have other 78 arguments, pass `false`. 79 80 Returns: 81 The result of calling `getopt` 82 83 ***************************************************************************/ 84 85 public GetoptResult parse (ref string[] args, bool passThrough = true) 86 { 87 return getopt( 88 args, 89 // `caseInsensistive` is the default, but we need something 90 // with the same type for the ternary 91 passThrough ? config.keepEndOfOptions : config.caseInsensitive, 92 // Also the default, same reasoning 93 passThrough ? config.passThrough : config.noPassThrough, 94 "config|c", 95 "Path to the config file. Defaults to: " ~ this.config_path, 96 &this.config_path, 97 98 "override|O", 99 "Override a config file value\n" ~ 100 "Example: -O foo.bar=true -O dns=1.1.1.1 -O dns=2.2.2.2\n" ~ 101 "Array values are additive, other items are set to the last override", 102 &this.overridesHandler, 103 ); 104 } 105 } 106 107 /******************************************************************************* 108 109 Attempt to read and deserialize the config file at `path` into the `struct` 110 type `Config` and print any error on failure 111 112 This 'simple' overload of the more detailed `parseConfigFile` will attempt 113 to deserialize the content of the file at `path` into an instance of 114 `ConfigT`, and return a `Nullable` instance of it. 115 If an error happens, either because the file isn't readable or 116 the configuration has an issue, a message will be printed to `stderr`, 117 with colors if the output is a TTY, and a `null` instance will be returned. 118 119 The calling code can hence just read a config file via: 120 ``` 121 int main () 122 { 123 auto configN = parseConfigFileSimple!Config("config.yaml"); 124 if (configN.isNull()) return 1; // Error path 125 auto config = configN.get(); 126 // Rest of the program ... 127 } 128 ``` 129 An overload accepting `CLIArgs args` also exists. 130 131 Params: 132 path = Path of the file to read from 133 args = Command line arguments on which `parse` has been called 134 strict = Whether the parsing should reject unknown keys in the 135 document, warn, or ignore them (default: `StrictMode.Error`) 136 137 Returns: 138 An initialized `ConfigT` instance if reading/parsing was successful; 139 a `null` instance otherwise. 140 141 *******************************************************************************/ 142 143 public Nullable!ConfigT parseConfigFileSimple (ConfigT) 144 (string path, StrictMode strict = StrictMode.Error) 145 { 146 return wrapException(parseConfigFile!(ConfigT)(CLIArgs(path), strict)); 147 } 148 149 /// Ditto 150 public Nullable!ConfigT parseConfigFileSimple (ConfigT) 151 (in CLIArgs args, StrictMode strict = StrictMode.Error) 152 { 153 return wrapException(parseConfigFile!(ConfigT)(args, strict)); 154 } 155 156 /******************************************************************************* 157 158 Parses the config file or string and returns a `Config` instance. 159 160 Params: 161 ConfigT = A `struct` type used to drive the deserialization and 162 validation. This type definition is the most important aspect 163 of how Configy works. 164 165 args = command-line arguments (containing the path to the config) 166 path = When parsing a string, the path corresponding to it 167 data = A string containing a valid YAML document to be processed 168 strict = Whether the parsing should reject unknown keys in the 169 document, warn, or ignore them (default: `StrictMode.Error`) 170 171 Throws: 172 `ConfigException` if deserializing the configuration into `ConfigT` 173 failed, or an underlying `Exception` if a backend failed (e.g. 174 `path` was not found). 175 176 Returns: 177 A valid `ConfigT` instance 178 179 *******************************************************************************/ 180 181 public ConfigT parseConfigFile (ConfigT) 182 (in CLIArgs args, StrictMode strict = StrictMode.Error) 183 { 184 import dub.internal.configy.backend.yaml; 185 186 auto root = parseFile(args.config_path); 187 return parseConfig!ConfigT(root, strict); 188 } 189 190 /// ditto 191 public ConfigT parseConfigString (ConfigT) 192 (string data, string path, StrictMode strict = StrictMode.Error) 193 { 194 CLIArgs args = { config_path: path }; 195 return parseConfigString!(ConfigT)(data, args, strict); 196 } 197 198 /// ditto 199 public ConfigT parseConfigString (ConfigT) 200 (string data, in CLIArgs args, StrictMode strict = StrictMode.Error) 201 { 202 import dub.internal.configy.backend.yaml; 203 204 assert(args.config_path.length, "No config_path provided to parseConfigString"); 205 auto root = parseString(data, args.config_path); 206 return parseConfig!ConfigT(root, strict); 207 }