1 /******************************************************************************* 2 3 Define UDAs that can be applied to a configuration struct 4 5 This module is stand alone (a leaf module) to allow importing the UDAs 6 without importing the whole configuration parsing code. 7 8 Copyright: 9 Copyright (c) 2019-2022 BOSAGORA Foundation 10 All rights reserved. 11 12 License: 13 MIT License. See LICENSE for details. 14 15 *******************************************************************************/ 16 17 module dub.internal.configy.attributes; 18 19 import std.traits; 20 21 /******************************************************************************* 22 23 An optional parameter with an initial value of `T.init` 24 25 The config parser automatically recognize non-default initializer, 26 so that the following: 27 ``` 28 public struct Config 29 { 30 public string greeting = "Welcome home"; 31 } 32 ``` 33 Will not error out if `greeting` is not defined in the config file. 34 However, this relies on the initializer of the field (`greeting`) being 35 different from the type initializer (`string.init` is `null`). 36 In some cases, the default value is also the desired initializer, e.g.: 37 ``` 38 public struct Config 39 { 40 /// Maximum number of connections. 0 means unlimited. 41 public uint connections_limit = 0; 42 } 43 ``` 44 In this case, one can add `@Optional` to the field to inform the parser. 45 46 *******************************************************************************/ 47 48 public struct Optional {} 49 50 /******************************************************************************* 51 52 Inform the config filler that this sequence is to be read as a mapping 53 54 On some occasions, one might want to read a mapping as an array. 55 One reason to do so may be to provide a better experience to the user, 56 e.g. having to type: 57 ``` 58 interfaces: 59 eth0: 60 ip: "192.168.0.1" 61 private: true 62 wlan0: 63 ip: "1.2.3.4" 64 ``` 65 Instead of the slightly more verbose: 66 ``` 67 interfaces: 68 - name: eth0 69 ip: "192.168.0.1" 70 private: true 71 - name: wlan0 72 ip: "1.2.3.4" 73 ``` 74 75 The former would require to be expressed as an associative arrays. 76 However, one major drawback of associative arrays is that they can't have 77 an initializer, which makes them cumbersome to use in the context of the 78 config filler. To remediate this issue, one may use `@Key("name")` 79 on a field (here, `interfaces`) so that the mapping is flattened 80 to an array. If `name` is `null`, the key will be discarded. 81 82 *******************************************************************************/ 83 84 public struct Key 85 { 86 /// 87 public string name; 88 } 89 90 /******************************************************************************* 91 92 Look up the provided name in the YAML node, instead of the field name. 93 94 By default, the config filler will look up the field name of a mapping in 95 the YAML node. If this is not desired, an explicit `Name` attribute can 96 be given. This is especially useful for names which are keyword. 97 98 ``` 99 public struct Config 100 { 101 public @Name("delete") bool remove; 102 } 103 ``` 104 105 *******************************************************************************/ 106 107 public struct Name 108 { 109 /// 110 public string name; 111 112 /// 113 public bool startsWith; 114 } 115 116 /// Short hand syntax 117 public Name StartsWith(string name) @safe pure nothrow @nogc 118 { 119 return Name(name, true); 120 } 121 122 /******************************************************************************* 123 124 A field which carries informations about whether it was set or not 125 126 Some configurations may need to know which fields were set explicitly while 127 keeping defaults. An example of this is a `struct` where at least one field 128 needs to be set, such as the following: 129 ``` 130 public struct ProtoDuration 131 { 132 public @Optional long weeks; 133 public @Optional long days; 134 public @Optional long hours; 135 public @Optional long minutes; 136 public long seconds = 42; 137 public @Optional long msecs; 138 public @Optional long usecs; 139 public @Optional long hnsecs; 140 public @Optional long nsecs; 141 } 142 ``` 143 In this case, it would be impossible to know if any field was explicitly 144 provided. Hence, the struct should be written as: 145 ``` 146 public struct ProtoDuration 147 { 148 public SetInfo!long weeks; 149 public SetInfo!long days; 150 public SetInfo!long hours; 151 public SetInfo!long minutes; 152 public SetInfo!long seconds = 42; 153 public SetInfo!long msecs; 154 public SetInfo!long usecs; 155 public SetInfo!long hnsecs; 156 public SetInfo!long nsecs; 157 } 158 ``` 159 Note that `SetInfo` implies `Optional`, and supports default values. 160 161 *******************************************************************************/ 162 163 public struct SetInfo (T) 164 { 165 /*************************************************************************** 166 167 Allow initialization as a field 168 169 This sets the field as having been set, so that: 170 ``` 171 struct Config { SetInfo!Duration timeout; } 172 173 Config myConf = { timeout: 10.minutes } 174 ``` 175 Will behave as if set explicitly. If this behavior is not wanted, 176 pass `false` as second argument: 177 ``` 178 Config myConf = { timeout: SetInfo!Duration(10.minutes, false) } 179 ``` 180 181 ***************************************************************************/ 182 183 public this (T initVal, bool isSet = true) @safe pure nothrow @nogc 184 { 185 this.value = initVal; 186 this.set = isSet; 187 } 188 189 /// Underlying data 190 public T value; 191 192 /// 193 alias value this; 194 195 /// Whether this field was set or not 196 public bool set; 197 } 198 199 /******************************************************************************* 200 201 Interface that is passed to `fromConfig` hook 202 203 The `ConfigParser` exposes the raw underlying node (see `node` method), 204 the path within the file (`path` method), and a simple ability to recurse 205 via `parseAs`. This allows to implement complex logic independent of the 206 underlying configuration format. 207 208 *******************************************************************************/ 209 210 public interface ConfigParser 211 { 212 import dub.internal.configy.backend.node; 213 import dub.internal.configy.fieldref : StructFieldRef; 214 import dub.internal.configy.read : Context, parseField; 215 216 /// Returns: the node being processed 217 public inout(Node) node () inout @safe pure nothrow @nogc; 218 219 /// Returns: current location we are parsing 220 public string path () const @safe pure nothrow @nogc; 221 222 /*************************************************************************** 223 224 Parse this struct as another type 225 226 This allows implementing union-like behavior, where a `struct` which 227 implements `fromConfig` can parse a simple representation as one type, 228 and one more advanced as another type. 229 230 Params: 231 OtherType = The type to parse as 232 defaultValue = The instance to use as a default value for fields 233 234 ***************************************************************************/ 235 236 public final auto parseAs (OtherType) 237 (auto ref OtherType defaultValue = OtherType.init) 238 { 239 alias TypeFieldRef = StructFieldRef!OtherType; 240 return this.node().parseField!(TypeFieldRef)( 241 this.path(), defaultValue, this.context()); 242 } 243 244 /// Internal use only 245 protected const(Context) context () const @safe pure nothrow @nogc; 246 } 247 248 /******************************************************************************* 249 250 Specify that a field only accept a limited set of string values. 251 252 This is similar to how `enum` symbolic names are treated, however the `enum` 253 symbolic names may not contain spaces or special character. 254 255 Params: 256 Values = Permissible values (case sensitive) 257 258 *******************************************************************************/ 259 260 public struct Only (string[] Values) { 261 public string value; 262 263 alias value this; 264 265 public static Only fromString (scope string str) { 266 import std.algorithm.searching : canFind; 267 import std.exception : enforce; 268 import std.format; 269 270 enforce(Values.canFind(str), 271 "%s is not a valid value for this field, valid values are: %(%s, %)" 272 .format(str, Values)); 273 return Only(str); 274 } 275 } 276 277 /// 278 unittest { 279 import dub.internal.configy.attributes : Only, Optional; 280 import dub.internal.configy.easy : parseConfigString; 281 282 static struct CountryConfig { 283 Only!(["France", "Malta", "South Korea"]) country; 284 // Compose with other attributes too 285 @Optional Only!(["citizen", "resident", "alien"]) status; 286 } 287 static struct Config { 288 CountryConfig[] countries; 289 } 290 291 auto conf = parseConfigString!Config(`countries: 292 - country: France 293 status: citizen 294 - country: Malta 295 - country: South Korea 296 status: alien 297 `, "/dev/null"); 298 299 assert(conf.countries.length == 3); 300 assert(conf.countries[0].country == `France`); 301 assert(conf.countries[0].status == `citizen`); 302 assert(conf.countries[1].country == `Malta`); 303 assert(conf.countries[1].status is null); 304 assert(conf.countries[2].country == `South Korea`); 305 assert(conf.countries[2].status == `alien`); 306 307 import dub.internal.configy.exceptions : ConfigException; 308 309 try parseConfigString!Config(`countries: 310 - country: France 311 status: expatriate 312 `, "/etc/config"); 313 catch (ConfigException exc) 314 assert(exc.toString() == `/etc/config(3:13): countries[0].status: expatriate is not a valid value for this field, valid values are: "citizen", "resident", "alien"`); 315 }