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 information 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 Provides a means to convert a field from a `Node` to a complex type 202 203 When filling the config, it might be useful to store types which are 204 not only simple `string` and integer, such as `URL`, `BigInt`, or any other 205 library type not directly under the user's control. 206 207 To allow reading those values from the config file, a `Converter` may 208 be used. The converter will tell the `ConfigFiller` how to convert from 209 `Node` to the desired type `T`. 210 211 If the type is under the user's control, one can also add a constructor 212 accepting a single string, or define the `fromString` method, both of which 213 are tried if no `Converter` is found. 214 215 For types not under the user's control, there might be different ways 216 to parse the same type within the same struct, or neither the ctor nor 217 the `fromString` method may be defined under that name. 218 The exmaple below uses `parse` in place of `fromString`, for example. 219 220 ``` 221 /// Complex structure representing the age of a person based on its birthday 222 public struct Age 223 { 224 /// 225 public uint birth_year; 226 /// 227 public uint birth_month; 228 /// 229 public uint birth_day; 230 231 /// Note that this will be picked up automatically if named `fromString` 232 /// but this struct might be a library type. 233 public static Age parse (string value) { /+ Magic +/ } 234 } 235 236 public struct Person 237 { 238 /// 239 @Converter!Age((Node value) => Age.parse(value.as!string)) 240 public Age age; 241 } 242 ``` 243 244 Note that some fields may also be of multiple YAML types, such as DUB's 245 `dependencies`, which is either a simple string (`"vibe-d": "~>1.0 "`), 246 or an in its complex form (`"vibe-d": { "version": "~>1.0" }`). 247 For those use cases, a `Converter` is the best approach. 248 249 To avoid repeating the field type, a convenience function is provided: 250 ``` 251 public struct Age 252 { 253 public uint birth_year; 254 public uint birth_month; 255 public uint birth_day; 256 public static Age parse (string value) { /+ Magic +/ } 257 } 258 259 public struct Person 260 { 261 /// Here `converter` will deduct the type from the delegate argument, 262 /// and return an instance of `Converter`. Mind the case. 263 @converter((Node value) => Age.parse(value.as!string)) 264 public Age age; 265 } 266 ``` 267 268 *******************************************************************************/ 269 270 public struct Converter (T) 271 { 272 /// 273 public alias ConverterFunc = T function (scope ConfigParser!T context); 274 275 /// 276 public ConverterFunc converter; 277 } 278 279 /// Ditto 280 public auto converter (FT) (FT func) 281 { 282 static assert(isFunctionPointer!FT, 283 "Error: Argument to `converter` should be a function pointer, not: " 284 ~ FT.stringof); 285 286 alias RType = ReturnType!FT; 287 static assert(!is(RType == void), 288 "Error: Converter needs to be of the return type of the field, not `void`"); 289 return Converter!RType(func); 290 } 291 292 /******************************************************************************* 293 294 Interface that is passed to `fromYAML` hook 295 296 The `ConfigParser` exposes the raw YAML node (`see `node` method), 297 the path within the file (`path` method), and a simple ability to recurse 298 via `parseAs`. 299 300 Params: 301 T = The type of the structure which defines a `fromYAML` hook 302 303 *******************************************************************************/ 304 305 public interface ConfigParser (T) 306 { 307 import dub.internal.dyaml.node; 308 import dub.internal.configy.FieldRef : StructFieldRef; 309 import dub.internal.configy.Read : Context, parseField; 310 311 /// Returns: the node being processed 312 public inout(Node) node () inout @safe pure nothrow @nogc; 313 314 /// Returns: current location we are parsing 315 public string path () const @safe pure nothrow @nogc; 316 317 /*************************************************************************** 318 319 Parse this struct as another type 320 321 This allows implementing union-like behavior, where a `struct` which 322 implements `fromYAML` can parse a simple representation as one type, 323 and one more advanced as another type. 324 325 Params: 326 OtherType = The type to parse as 327 defaultValue = The instance to use as a default value for fields 328 329 ***************************************************************************/ 330 331 public final auto parseAs (OtherType) 332 (auto ref OtherType defaultValue = OtherType.init) 333 { 334 alias TypeFieldRef = StructFieldRef!OtherType; 335 return this.node().parseField!(TypeFieldRef)( 336 this.path(), defaultValue, this.context()); 337 } 338 339 /// Internal use only 340 protected const(Context) context () const @safe pure nothrow @nogc; 341 }