1 /******************************************************************************* 2 3 Utilities to fill a struct representing the configuration with the content 4 of a YAML document. 5 6 The main function of this module is `parseConfig`. Convenience functions 7 `parseConfigString` and `parseConfigFile` are also available. 8 9 The type parameter to those three functions must be a struct and is used 10 to drive the processing of the YAML node. When an error is encountered, 11 an `Exception` will be thrown, with a descriptive message. 12 The rules by which the struct is filled are designed to be 13 as intuitive as possible, and are described below. 14 15 Optional_Fields: 16 One of the major convenience offered by this utility is its handling 17 of optional fields. A field is detected as optional if it has 18 an initializer that is different from its type `init` value, 19 for example `string field = "Something";` is an optional field, 20 but `int count = 0;` is not. 21 To mark a field as optional even with its default value, 22 use the `Optional` UDA: `@Optional int count = 0;`. 23 24 Converter: 25 Because config structs may contain complex types such as 26 a Phobos type, a user-defined `Amount`, or Vibe.d's `URL`, 27 one may need to apply a converter to a struct's field. 28 Converters are functions that take a YAML `Node` as argument 29 and return a type that is implicitly convertible to the field type 30 (usually just the field type). They offer the most power to users, 31 as they can inspect the YAML structure, but should be used as a last resort. 32 33 Composite_Types: 34 Processing starts from a `struct` at the top level, and recurse into 35 every fields individually. If a field is itself a struct, 36 the filler will attempt the following, in order: 37 - If the field has no value and is not optional, an Exception will 38 be thrown with an error message detailing where the issue happened. 39 - If the field has no value and is optional, the default value will 40 be used. 41 - If the field has a value, the filler will first check for a converter 42 and use it if present. 43 - If the type has a `static` method named `fromString` whose sole argument 44 is a `string`, it will be used. 45 - If the type has a constructor whose sole argument is a `string`, 46 it will be used; 47 - Finally, the filler will attempt to deserialize all struct members 48 one by one and pass them to the default constructor, if there is any. 49 - If none of the above succeeded, a `static assert` will trigger. 50 51 Alias_this: 52 If a `struct` contains an `alias this`, the field that is aliased will be 53 ignored, instead the config parser will parse nested fields as if they 54 were part of the enclosing structure. This allow to re-use a single `struct` 55 in multiple place without having to resort to a `mixin template`. 56 Having an initializer will make all fields in the aliased struct optional. 57 The aliased field cannot have attributes other than `@Optional`, 58 which will then apply to all fields it exposes. 59 60 Duration_parsing: 61 If the config field is of type `core.time.Duration`, special parsing rules 62 will apply. There are two possible forms in which a Duration field may 63 be expressed. In the first form, the YAML node should be a mapping, 64 and it will be checked for fields matching the supported units 65 in `core.time`: `weeks`, `days`, `hours`, `minutes`, `seconds`, `msecs`, 66 `usecs`, `hnsecs`, `nsecs`. Strict parsing option will be respected. 67 The values of the fields will then be added together, so the following 68 YAML usages are equivalent: 69 --- 70 // sleepFor: 71 // hours: 8 72 // minutes: 30 73 --- 74 and: 75 --- 76 // sleepFor: 77 // minutes: 510 78 --- 79 Provided that the definition of the field is: 80 --- 81 public Duration sleepFor; 82 --- 83 84 In the second form, the field should have a suffix composed of an 85 underscore ('_'), followed by a unit name as defined in `core.time`. 86 This can be either the field name directly, or a name override. 87 The latter is recommended to avoid confusion when using the field in code. 88 In this form, the YAML node is expected to be a scalar. 89 So the previous example, using this form, would be expressed as: 90 --- 91 sleepFor_minutes: 510 92 --- 93 and the field definition should be one of those two: 94 --- 95 public @Name("sleepFor_minutes") Duration sleepFor; /// Prefer this 96 public Duration sleepFor_minutes; /// This works too 97 --- 98 99 Those forms are mutually exclusive, so a field with a unit suffix 100 will error out if a mapping is used. This prevents surprises and ensures 101 that the error message, if any, is consistent across user input. 102 103 To disable or change this behavior, one may use a `Converter` instead. 104 105 Strict_Parsing: 106 When strict parsing is enabled, the config filler will also validate 107 that the YAML nodes do not contains entry which are not present in the 108 mapping (struct) being processed. 109 This can be useful to catch typos or outdated configuration options. 110 111 Post_Validation: 112 Some configuration will require validation across multiple sections. 113 For example, two sections may be mutually exclusive as a whole, 114 or may have fields which are mutually exclusive with another section's 115 field(s). This kind of dependence is hard to account for declaratively, 116 and does not affect parsing. For this reason, the preferred way to 117 handle those cases is to define a `validate` member method on the 118 affected config struct(s), which will be called once 119 parsing for that mapping is completed. 120 If an error is detected, this method should throw an Exception. 121 122 Enabled_or_disabled_field: 123 While most complex logic validation should be handled post-parsing, 124 some section may be optional by default, but if provided, will have 125 required fields. To support this use case, if a field with the name 126 `enabled` is present in a struct, the parser will first process it. 127 If it is `false`, the parser will not attempt to process the struct 128 further, and the other fields will have their default value. 129 Likewise, if a field named `disabled` exists, the struct will not 130 be processed if it is set to `true`. 131 132 Copyright: 133 Copyright (c) 2019-2022 BOSAGORA Foundation 134 All rights reserved. 135 136 License: 137 MIT License. See LICENSE for details. 138 139 *******************************************************************************/ 140 141 module dub.internal.configy.Read; 142 143 public import dub.internal.configy.Attributes; 144 public import dub.internal.configy.Exceptions : ConfigException; 145 import dub.internal.configy.Exceptions; 146 import dub.internal.configy.FieldRef; 147 import dub.internal.configy.Utils; 148 149 import dub.internal.dyaml.exception; 150 import dub.internal.dyaml.node; 151 import dub.internal.dyaml.loader; 152 153 import std.algorithm; 154 import std.conv; 155 import std.datetime; 156 import std.format; 157 import std.getopt; 158 import std.meta; 159 import std.range; 160 import std.traits; 161 import std.typecons : Nullable, nullable, tuple; 162 163 static import core.time; 164 165 // Dub-specific adjustments for output 166 import dub.internal.logging; 167 168 /// Command-line arguments 169 public struct CLIArgs 170 { 171 /// Path to the config file 172 public string config_path = "config.yaml"; 173 174 /// Overrides for config options 175 public string[][string] overrides; 176 177 /// Helper to add items to `overrides` 178 public void overridesHandler (string, string value) 179 { 180 import std.string; 181 const idx = value.indexOf('='); 182 if (idx < 0) return; 183 string k = value[0 .. idx], v = value[idx + 1 .. $]; 184 if (auto val = k in this.overrides) 185 (*val) ~= v; 186 else 187 this.overrides[k] = [ v ]; 188 } 189 190 /*************************************************************************** 191 192 Parses the base command line arguments 193 194 This can be composed with the program argument. 195 For example, consider a program which wants to expose a `--version` 196 switch, the definition could look like this: 197 --- 198 public struct ProgramCLIArgs 199 { 200 public CLIArgs base; // This struct 201 202 public alias base this; // For convenience 203 204 public bool version_; // Program-specific part 205 } 206 --- 207 Then, an application-specific configuration routine would be: 208 --- 209 public GetoptResult parse (ref ProgramCLIArgs clargs, ref string[] args) 210 { 211 auto r = clargs.base.parse(args); 212 if (r.helpWanted) return r; 213 return getopt( 214 args, 215 "version", "Print the application version, &clargs.version_"); 216 } 217 --- 218 219 Params: 220 args = The command line args to parse (parsed options will be removed) 221 passThrough = Whether to enable `config.passThrough` and 222 `config.keepEndOfOptions`. `true` by default, to allow 223 composability. If your program doesn't have other 224 arguments, pass `false`. 225 226 Returns: 227 The result of calling `getopt` 228 229 ***************************************************************************/ 230 231 public GetoptResult parse (ref string[] args, bool passThrough = true) 232 { 233 return getopt( 234 args, 235 // `caseInsensitive` is the default, but we need something 236 // with the same type for the ternary 237 passThrough ? config.keepEndOfOptions : config.caseInsensitive, 238 // Also the default, same reasoning 239 passThrough ? config.passThrough : config.noPassThrough, 240 "config|c", 241 "Path to the config file. Defaults to: " ~ this.config_path, 242 &this.config_path, 243 244 "override|O", 245 "Override a config file value\n" ~ 246 "Example: -O foo.bar=true -o dns=1.1.1.1 -o dns=2.2.2.2\n" ~ 247 "Array values are additive, other items are set to the last override", 248 &this.overridesHandler, 249 ); 250 } 251 } 252 253 /******************************************************************************* 254 255 Attempt to read and process the config file at `path`, print any error 256 257 This 'simple' overload of the more detailed `parseConfigFile` will attempt 258 to read the file at `path`, and return a `Nullable` instance of it. 259 If an error happens, either because the file isn't readable or 260 the configuration has an issue, a message will be printed to `stderr`, 261 with colors if the output is a TTY, and a `null` instance will be returned. 262 263 The calling code can hence just read a config file via: 264 ``` 265 int main () 266 { 267 auto configN = parseConfigFileSimple!Config("config.yaml"); 268 if (configN.isNull()) return 1; // Error path 269 auto config = configN.get(); 270 // Rest of the program ... 271 } 272 ``` 273 An overload accepting `CLIArgs args` also exists. 274 275 Params: 276 path = Path of the file to read from 277 args = Command line arguments on which `parse` has been called 278 strict = Whether the parsing should reject unknown keys in the 279 document, warn, or ignore them (default: `StrictMode.Error`) 280 281 Returns: 282 An initialized `Config` instance if reading/parsing was successful; 283 a `null` instance otherwise. 284 285 *******************************************************************************/ 286 287 public Nullable!T parseConfigFileSimple (T) (string path, StrictMode strict = StrictMode.Error) 288 { 289 return parseConfigFileSimple!(T)(CLIArgs(path), strict); 290 } 291 292 293 /// Ditto 294 public Nullable!T parseConfigFileSimple (T) (in CLIArgs args, StrictMode strict = StrictMode.Error) 295 { 296 try 297 { 298 Node root = Loader.fromFile(args.config_path).load(); 299 return nullable(parseConfig!T(args, root, strict)); 300 } 301 catch (ConfigException exc) 302 { 303 exc.printException(); 304 return typeof(return).init; 305 } 306 catch (Exception exc) 307 { 308 // Other Exception type may be thrown by D-YAML, 309 // they won't include rich information. 310 logWarn("%s", exc.message()); 311 return typeof(return).init; 312 } 313 } 314 315 /******************************************************************************* 316 317 Print an Exception, potentially with colors on 318 319 Trusted because of `stderr` usage. 320 321 *******************************************************************************/ 322 323 private void printException (scope ConfigException exc) @trusted 324 { 325 import dub.internal.logging; 326 327 if (hasColors) 328 logWarn("%S", exc); 329 else 330 logWarn("%s", exc.message()); 331 } 332 333 /******************************************************************************* 334 335 Parses the config file or string and returns a `Config` instance. 336 337 Params: 338 cmdln = command-line arguments (containing the path to the config) 339 path = When parsing a string, the path corresponding to it 340 strict = Whether the parsing should reject unknown keys in the 341 document, warn, or ignore them (default: `StrictMode.Error`) 342 343 Throws: 344 `Exception` if parsing the config file failed. 345 346 Returns: 347 `Config` instance 348 349 *******************************************************************************/ 350 351 public T parseConfigFile (T) (in CLIArgs cmdln, StrictMode strict = StrictMode.Error) 352 { 353 Node root = Loader.fromFile(cmdln.config_path).load(); 354 return parseConfig!T(cmdln, root, strict); 355 } 356 357 /// ditto 358 public T parseConfigString (T) (string data, string path, StrictMode strict = StrictMode.Error) 359 { 360 CLIArgs cmdln = { config_path: path }; 361 auto loader = Loader.fromString(data); 362 loader.name = path; 363 Node root = loader.load(); 364 return parseConfig!T(cmdln, root, strict); 365 } 366 367 /******************************************************************************* 368 369 Process the content of the YAML document described by `node` into an 370 instance of the struct `T`. 371 372 See the module description for a complete overview of this function. 373 374 Params: 375 T = Type of the config struct to fill 376 cmdln = Command line arguments 377 node = The root node matching `T` 378 strict = Action to take when encountering unknown keys in the document 379 380 Returns: 381 An instance of `T` filled with the content of `node` 382 383 Throws: 384 If the content of `node` cannot satisfy the requirements set by `T`, 385 or if `node` contain extra fields and `strict` is `true`. 386 387 *******************************************************************************/ 388 389 public T parseConfig (T) ( 390 in CLIArgs cmdln, Node node, StrictMode strict = StrictMode.Error) 391 { 392 static assert(is(T == struct), "`" ~ __FUNCTION__ ~ 393 "` should only be called with a `struct` type as argument, not: `" ~ 394 fullyQualifiedName!T ~ "`"); 395 396 final switch (node.nodeID) 397 { 398 case NodeID.mapping: 399 dbgWrite("Parsing config '%s', strict: %s", 400 fullyQualifiedName!T, 401 strict == StrictMode.Warn ? 402 strict.paint(Yellow) : strict.paintIf(!!strict, Green, Red)); 403 return node.parseMapping!(StructFieldRef!T)( 404 null, T.init, const(Context)(cmdln, strict), null); 405 case NodeID.sequence: 406 case NodeID.scalar: 407 case NodeID.invalid: 408 throw new TypeConfigException(node, "mapping (object)", "document root"); 409 } 410 } 411 412 /******************************************************************************* 413 414 The behavior to have when encountering a field in YAML not present 415 in the config definition. 416 417 *******************************************************************************/ 418 419 public enum StrictMode 420 { 421 /// Issue an error by throwing an `UnknownKeyConfigException` 422 Error = 0, 423 /// Write a message to `stderr`, but continue processing the file 424 Warn = 1, 425 /// Be silent and do nothing 426 Ignore = 2, 427 } 428 429 /// Used to pass around configuration 430 package struct Context 431 { 432 /// 433 private CLIArgs cmdln; 434 435 /// 436 private StrictMode strict; 437 } 438 439 /******************************************************************************* 440 441 Parse a mapping from `node` into an instance of `T` 442 443 Params: 444 TLFR = Top level field reference for this mapping 445 node = The YAML node object matching the struct being read 446 path = The runtime path to this mapping, used for nested types 447 defaultValue = The default value to use for `T`, which can be different 448 from `T.init` when recursing into fields with initializers. 449 ctx = A context where properties that need to be conserved during 450 recursion are stored 451 fieldDefaults = Default value for some fields, used for `Key` recursion 452 453 *******************************************************************************/ 454 private TLFR.Type parseMapping (alias TLFR) 455 (Node node, string path, auto ref TLFR.Type defaultValue, 456 in Context ctx, in Node[string] fieldDefaults) 457 { 458 static assert(is(TLFR.Type == struct), "`parseMapping` called with wrong type (should be a `struct`)"); 459 assert(node.nodeID == NodeID.mapping, "Internal error: parseMapping shouldn't have been called"); 460 461 dbgWrite("%s: `parseMapping` called for '%s' (node entries: %s)", 462 TLFR.Type.stringof.paint(Cyan), path.paint(Cyan), 463 node.length.paintIf(!!node.length, Green, Red)); 464 465 static foreach (FR; FieldRefTuple!(TLFR.Type)) 466 { 467 static if (FR.Name != FR.FieldName && hasMember!(TLFR.Type, FR.Name) && 468 !is(typeof(mixin("TLFR.Type.", FR.Name)) == function)) 469 static assert (FieldRef!(TLFR.Type, FR.Name).Name != FR.Name, 470 "Field `" ~ FR.FieldName ~ "` `@Name` attribute shadows field `" ~ 471 FR.Name ~ "` in `" ~ TLFR.Type.stringof ~ "`: Add a `@Name` attribute to `" ~ 472 FR.Name ~ "` or change that of `" ~ FR.FieldName ~ "`"); 473 } 474 475 if (ctx.strict != StrictMode.Ignore) 476 { 477 /// First, check that all the sections found in the mapping are present in the type 478 /// If not, the user might have made a typo. 479 immutable string[] fieldNames = [ FieldsName!(TLFR.Type) ]; 480 immutable string[] patterns = [ Patterns!(TLFR.Type) ]; 481 FIELD: foreach (const ref Node key, const ref Node value; node) 482 { 483 const k = key.as!string; 484 if (!fieldNames.canFind(k)) 485 { 486 foreach (p; patterns) 487 if (k.startsWith(p)) 488 // Require length because `0` would match `canFind` 489 // and we don't want to allow `$PATTERN-` 490 if (k[p.length .. $].length > 1 && k[p.length] == '-') 491 continue FIELD; 492 493 if (ctx.strict == StrictMode.Warn) 494 { 495 scope exc = new UnknownKeyConfigException( 496 path, key.as!string, fieldNames, key.startMark()); 497 exc.printException(); 498 } 499 else 500 throw new UnknownKeyConfigException( 501 path, key.as!string, fieldNames, key.startMark()); 502 } 503 } 504 } 505 506 const enabledState = node.isMappingEnabled!(TLFR.Type)(defaultValue); 507 508 if (enabledState.field != EnabledState.Field.None) 509 dbgWrite("%s: Mapping is enabled: %s", TLFR.Type.stringof.paint(Cyan), (!!enabledState).paintBool()); 510 511 auto convertField (alias FR) () 512 { 513 static if (FR.Name != FR.FieldName) 514 dbgWrite("Field name `%s` will use YAML field `%s`", 515 FR.FieldName.paint(Yellow), FR.Name.paint(Green)); 516 // Using exact type here matters: we could get a qualified type 517 // (e.g. `immutable(string)`) if the field is qualified, 518 // which causes problems. 519 FR.Type default_ = __traits(getMember, defaultValue, FR.FieldName); 520 521 // If this struct is disabled, do not attempt to parse anything besides 522 // the `enabled` / `disabled` field. 523 if (!enabledState) 524 { 525 // Even this is too noisy 526 version (none) 527 dbgWrite("%s: %s field of disabled struct, default: %s", 528 path.paint(Cyan), "Ignoring".paint(Yellow), default_); 529 530 static if (FR.Name == "enabled") 531 return false; 532 else static if (FR.Name == "disabled") 533 return true; 534 else 535 return default_; 536 } 537 538 if (auto ptr = FR.FieldName in fieldDefaults) 539 { 540 dbgWrite("Found %s (%s.%s) in `fieldDefaults`", 541 FR.Name.paint(Cyan), path.paint(Cyan), FR.FieldName.paint(Cyan)); 542 543 if (ctx.strict && FR.FieldName in node) 544 throw new ConfigExceptionImpl("'Key' field is specified twice", path, FR.FieldName, node.startMark()); 545 return (*ptr).parseField!(FR)(path.addPath(FR.FieldName), default_, ctx) 546 .dbgWriteRet("Using value '%s' from fieldDefaults for field '%s'", 547 FR.FieldName.paint(Cyan)); 548 } 549 550 // This, `FR.Pattern`, and the field in `@Name` are special support for `dub` 551 static if (FR.Pattern) 552 { 553 static if (is(FR.Type : V[K], K, V)) 554 { 555 alias AAFieldRef = NestedFieldRef!(V, FR); 556 static assert(is(K : string), "Key type should be string-like"); 557 } 558 else 559 static assert(0, "Cannot have pattern on non-AA field"); 560 561 AAFieldRef.Type[string] result; 562 foreach (pair; node.mapping) 563 { 564 const key = pair.key.as!string; 565 if (!key.startsWith(FR.Name)) 566 continue; 567 string suffix = key[FR.Name.length .. $]; 568 if (suffix.length) 569 { 570 if (suffix[0] == '-') suffix = suffix[1 .. $]; 571 else continue; 572 } 573 574 result[suffix] = pair.value.parseField!(AAFieldRef)( 575 path.addPath(key), default_.get(key, AAFieldRef.Type.init), ctx); 576 } 577 bool hack = true; 578 if (hack) return result; 579 } 580 581 if (auto ptr = FR.Name in node) 582 { 583 dbgWrite("%s: YAML field is %s in node%s", 584 FR.Name.paint(Cyan), "present".paint(Green), 585 (FR.Name == FR.FieldName ? "" : " (note that field name is overriden)").paint(Yellow)); 586 return (*ptr).parseField!(FR)(path.addPath(FR.Name), default_, ctx) 587 .dbgWriteRet("Using value '%s' from YAML document for field '%s'", 588 FR.FieldName.paint(Cyan)); 589 } 590 591 dbgWrite("%s: Field is %s from node%s", 592 FR.Name.paint(Cyan), "missing".paint(Red), 593 (FR.Name == FR.FieldName ? "" : " (note that field name is overriden)").paint(Yellow)); 594 595 // A field is considered optional if it has an initializer that is different 596 // from its default value, or if it has the `Optional` UDA. 597 // In that case, just return this value. 598 static if (FR.Optional) 599 return default_ 600 .dbgWriteRet("Using default value '%s' for optional field '%s'", FR.FieldName.paint(Cyan)); 601 602 // The field is not present, but it could be because it is an optional section. 603 // For example, the section could be defined as: 604 // --- 605 // struct RequestLimit { size_t reqs = 100; } 606 // struct Config { RequestLimit limits; } 607 // --- 608 // In this case we need to recurse into `RequestLimit` to check if any 609 // of its field is required. 610 else static if (mightBeOptional!FR) 611 { 612 const npath = path.addPath(FR.Name); 613 string[string] aa; 614 return Node(aa).parseMapping!(FR)(npath, default_, ctx, null); 615 } 616 else 617 throw new MissingKeyException(path, FR.Name, node.startMark()); 618 } 619 620 FR.Type convert (alias FR) () 621 { 622 static if (__traits(getAliasThis, TLFR.Type).length == 1 && 623 __traits(getAliasThis, TLFR.Type)[0] == FR.FieldName) 624 { 625 static assert(FR.Name == FR.FieldName, 626 "Field `" ~ fullyQualifiedName!(FR.Ref) ~ 627 "` is the target of an `alias this` and cannot have a `@Name` attribute"); 628 static assert(!hasConverter!(FR.Ref), 629 "Field `" ~ fullyQualifiedName!(FR.Ref) ~ 630 "` is the target of an `alias this` and cannot have a `@Converter` attribute"); 631 632 alias convertW(string FieldName) = convert!(FieldRef!(FR.Type, FieldName, FR.Optional)); 633 return FR.Type(staticMap!(convertW, FieldNameTuple!(FR.Type))); 634 } 635 else 636 return convertField!(FR)(); 637 } 638 639 debug (ConfigFillerDebug) 640 { 641 indent++; 642 scope (exit) indent--; 643 } 644 645 TLFR.Type doValidation (TLFR.Type result) 646 { 647 static if (is(typeof(result.validate()))) 648 { 649 if (enabledState) 650 { 651 dbgWrite("%s: Calling `%s` method", 652 TLFR.Type.stringof.paint(Cyan), "validate()".paint(Green)); 653 result.validate(); 654 } 655 else 656 { 657 dbgWrite("%s: Ignoring `%s` method on disabled mapping", 658 TLFR.Type.stringof.paint(Cyan), "validate()".paint(Green)); 659 } 660 } 661 else if (enabledState) 662 dbgWrite("%s: No `%s` method found", 663 TLFR.Type.stringof.paint(Cyan), "validate()".paint(Yellow)); 664 665 return result; 666 } 667 668 // This might trigger things like "`this` is not accessible". 669 // In this case, the user most likely needs to provide a converter. 670 alias convertWrapper(string FieldName) = convert!(FieldRef!(TLFR.Type, FieldName)); 671 return doValidation(TLFR.Type(staticMap!(convertWrapper, FieldNameTuple!(TLFR.Type)))); 672 } 673 674 /******************************************************************************* 675 676 Parse a field, trying to match up the compile-time expectation with 677 the run time value of the Node (`nodeID`). 678 679 This is the central point which does "type conversion", from the YAML node 680 to the field type. Whenever adding support for a new type, things should 681 happen here. 682 683 Because a `struct` can be filled from either a mapping or a scalar, 684 this function will first try the converter / fromString / string ctor 685 methods before defaulting to field-wise construction. 686 687 Note that optional fields are checked before recursion happens, 688 so this method does not do this check. 689 690 *******************************************************************************/ 691 692 package FR.Type parseField (alias FR) 693 (Node node, string path, auto ref FR.Type defaultValue, in Context ctx) 694 { 695 if (node.nodeID == NodeID.invalid) 696 throw new TypeConfigException(node, "valid", path); 697 698 // If we reached this, it means the field is set, so just recurse 699 // to peel the type 700 static if (is(FR.Type : SetInfo!FT, FT)) 701 return FR.Type( 702 parseField!(FieldRef!(FR.Type, "value"))(node, path, defaultValue, ctx), 703 true); 704 705 else static if (hasConverter!(FR.Ref)) 706 return wrapException(node.viaConverter!(FR)(path, ctx), path, node.startMark()); 707 708 else static if (hasFromYAML!(FR.Type)) 709 { 710 scope impl = new ConfigParserImpl!(FR.Type)(node, path, ctx); 711 return wrapException(FR.Type.fromYAML(impl), path, node.startMark()); 712 } 713 714 else static if (hasFromString!(FR.Type)) 715 return wrapException(FR.Type.fromString(node.as!string), path, node.startMark()); 716 717 else static if (hasStringCtor!(FR.Type)) 718 return wrapException(FR.Type(node.as!string), path, node.startMark()); 719 720 else static if (is(immutable(FR.Type) == immutable(core.time.Duration))) 721 { 722 if (node.nodeID != NodeID.mapping) 723 throw new DurationTypeConfigException(node, path); 724 return node.parseMapping!(StructFieldRef!DurationMapping)( 725 path, DurationMapping.make(defaultValue), ctx, null).opCast!Duration; 726 } 727 728 else static if (is(FR.Type == struct)) 729 { 730 if (node.nodeID != NodeID.mapping) 731 throw new TypeConfigException(node, "mapping (object)", path); 732 return node.parseMapping!(FR)(path, defaultValue, ctx, null); 733 } 734 735 // Handle string early as they match the sequence rule too 736 else static if (isSomeString!(FR.Type)) 737 // Use `string` type explicitly because `Variant` thinks 738 // `immutable(char)[]` (aka `string`) and `immutable(char[])` 739 // (aka `immutable(string)`) are not compatible. 740 return node.parseScalar!(string)(path); 741 // Enum too, as their base type might be an array (including strings) 742 else static if (is(FR.Type == enum)) 743 return node.parseScalar!(FR.Type)(path); 744 745 else static if (is(FR.Type : E[K], E, K)) 746 { 747 if (node.nodeID != NodeID.mapping) 748 throw new TypeConfigException(node, "mapping (associative array)", path); 749 750 // Note: As of June 2022 (DMD v2.100.0), associative arrays cannot 751 // have initializers, hence their UX for config is less optimal. 752 return node.mapping().map!( 753 (Node.Pair pair) { 754 return tuple( 755 pair.key.get!K, 756 pair.value.parseField!(NestedFieldRef!(E, FR))( 757 format("%s[%s]", path, pair.key.as!string), E.init, ctx)); 758 }).assocArray(); 759 760 } 761 else static if (is(FR.Type : E[], E)) 762 { 763 static if (hasUDA!(FR.Ref, Key)) 764 { 765 static assert(getUDAs!(FR.Ref, Key).length == 1, 766 "`" ~ fullyQualifiedName!(FR.Ref) ~ 767 "` field shouldn't have more than one `Key` attribute"); 768 static assert(is(E == struct), 769 "Field `" ~ fullyQualifiedName!(FR.Ref) ~ 770 "` has a `Key` attribute, but is a sequence of `" ~ 771 fullyQualifiedName!E ~ "`, not a sequence of `struct`"); 772 773 string key = getUDAs!(FR.Ref, Key)[0].name; 774 775 if (node.nodeID != NodeID.mapping && node.nodeID != NodeID.sequence) 776 throw new TypeConfigException(node, "mapping (object) or sequence", path); 777 778 if (node.nodeID == NodeID.mapping) return node.mapping().map!( 779 (Node.Pair pair) { 780 if (pair.value.nodeID != NodeID.mapping) 781 throw new TypeConfigException( 782 "sequence of " ~ pair.value.nodeTypeString(), 783 "sequence of mapping (array of objects)", 784 path, null, node.startMark()); 785 786 return pair.value.parseMapping!(StructFieldRef!E)( 787 path.addPath(pair.key.as!string), 788 E.init, ctx, key.length ? [ key: pair.key ] : null); 789 }).array(); 790 } 791 if (node.nodeID != NodeID.sequence) 792 throw new TypeConfigException(node, "sequence (array)", path); 793 794 // We pass `E.init` as default value as it is not going to be used: 795 // Either there is something in the YAML document, and that will be 796 // converted, or `sequence` will not iterate. 797 return node.sequence.enumerate.map!( 798 kv => kv.value.parseField!(NestedFieldRef!(E, FR))( 799 format("%s[%s]", path, kv.index), E.init, ctx)) 800 .array(); 801 } 802 else 803 { 804 static assert (!is(FR.Type == union), 805 "`union` are not supported. Use a converter instead"); 806 return node.parseScalar!(FR.Type)(path); 807 } 808 } 809 810 /// Parse a node as a scalar 811 private T parseScalar (T) (Node node, string path) 812 { 813 if (node.nodeID != NodeID.scalar) 814 throw new TypeConfigException(node, "scalar (value)", path); 815 816 static if (is(T == enum)) 817 return node.as!string.to!(T); 818 else 819 return node.as!(T); 820 } 821 822 /******************************************************************************* 823 824 Write a potentially throwing user-provided expression in ConfigException 825 826 The user-provided hooks may throw (e.g. `fromString / the constructor), 827 and the error may or may not be clear. We can't do anything about a bad 828 message but we can wrap the thrown exception in a `ConfigException` 829 to provide the location in the yaml file where the error happened. 830 831 Params: 832 exp = The expression that may throw 833 path = Path within the config file of the field 834 position = Position of the node in the YAML file 835 file = Call site file (otherwise the message would point to this function) 836 line = Call site line (see `file` reasoning) 837 838 Returns: 839 The result of `exp` evaluation. 840 841 *******************************************************************************/ 842 843 private T wrapException (T) (lazy T exp, string path, Mark position, 844 string file = __FILE__, size_t line = __LINE__) 845 { 846 try 847 return exp; 848 catch (ConfigException exc) 849 throw exc; 850 catch (Exception exc) 851 throw new ConstructionException(exc, path, position, file, line); 852 } 853 854 /// Allows us to reuse parseMapping and strict parsing 855 private struct DurationMapping 856 { 857 public SetInfo!long weeks; 858 public SetInfo!long days; 859 public SetInfo!long hours; 860 public SetInfo!long minutes; 861 public SetInfo!long seconds; 862 public SetInfo!long msecs; 863 public SetInfo!long usecs; 864 public SetInfo!long hnsecs; 865 public SetInfo!long nsecs; 866 867 private static DurationMapping make (Duration def) @safe pure nothrow @nogc 868 { 869 typeof(return) result; 870 auto fullSplit = def.split(); 871 result.weeks = SetInfo!long(fullSplit.weeks, fullSplit.weeks != 0); 872 result.days = SetInfo!long(fullSplit.days, fullSplit.days != 0); 873 result.hours = SetInfo!long(fullSplit.hours, fullSplit.hours != 0); 874 result.minutes = SetInfo!long(fullSplit.minutes, fullSplit.minutes != 0); 875 result.seconds = SetInfo!long(fullSplit.seconds, fullSplit.seconds != 0); 876 result.msecs = SetInfo!long(fullSplit.msecs, fullSplit.msecs != 0); 877 result.usecs = SetInfo!long(fullSplit.usecs, fullSplit.usecs != 0); 878 result.hnsecs = SetInfo!long(fullSplit.hnsecs, fullSplit.hnsecs != 0); 879 // nsecs is ignored by split as it's not representable in `Duration` 880 return result; 881 } 882 883 /// 884 public void validate () const @safe 885 { 886 // That check should never fail, as the YAML parser would error out, 887 // but better be safe than sorry. 888 foreach (field; this.tupleof) 889 if (field.set) 890 return; 891 892 throw new Exception( 893 "Expected at least one of the components (weeks, days, hours, " ~ 894 "minutes, seconds, msecs, usecs, hnsecs, nsecs) to be set"); 895 } 896 897 /// Allow conversion to a `Duration` 898 public Duration opCast (T : Duration) () const scope @safe pure nothrow @nogc 899 { 900 return core.time.weeks(this.weeks) + core.time.days(this.days) + 901 core.time.hours(this.hours) + core.time.minutes(this.minutes) + 902 core.time.seconds(this.seconds) + core.time.msecs(this.msecs) + 903 core.time.usecs(this.usecs) + core.time.hnsecs(this.hnsecs) + 904 core.time.nsecs(this.nsecs); 905 } 906 } 907 908 /// Evaluates to `true` if we should recurse into the struct via `parseMapping` 909 private enum mightBeOptional (alias FR) = is(FR.Type == struct) && 910 !is(immutable(FR.Type) == immutable(core.time.Duration)) && 911 !hasConverter!(FR.Ref) && !hasFromString!(FR.Type) && 912 !hasStringCtor!(FR.Type) && !hasFromYAML!(FR.Type); 913 914 /// Convenience template to check for the presence of converter(s) 915 private enum hasConverter (alias Field) = hasUDA!(Field, Converter); 916 917 /// Provided a field reference `FR` which is known to have at least one converter, 918 /// perform basic checks and return the value after applying the converter. 919 private auto viaConverter (alias FR) (Node node, string path, in Context context) 920 { 921 enum Converters = getUDAs!(FR.Ref, Converter); 922 static assert (Converters.length, 923 "Internal error: `viaConverter` called on field `" ~ 924 FR.FieldName ~ "` with no converter"); 925 926 static assert(Converters.length == 1, 927 "Field `" ~ FR.FieldName ~ "` cannot have more than one `Converter`"); 928 929 scope impl = new ConfigParserImpl!(FR.Type)(node, path, context); 930 return Converters[0].converter(impl); 931 } 932 933 private final class ConfigParserImpl (T) : ConfigParser!T 934 { 935 private Node node_; 936 private string path_; 937 private const(Context) context_; 938 939 /// Ctor 940 public this (Node n, string p, const Context c) scope @safe pure nothrow @nogc 941 { 942 this.node_ = n; 943 this.path_ = p; 944 this.context_ = c; 945 } 946 947 public final override inout(Node) node () inout @safe pure nothrow @nogc 948 { 949 return this.node_; 950 } 951 952 public final override string path () const @safe pure nothrow @nogc 953 { 954 return this.path_; 955 } 956 957 protected final override const(Context) context () const @safe pure nothrow @nogc 958 { 959 return this.context_; 960 } 961 } 962 963 /// Helper predicate 964 private template NameIs (string searching) 965 { 966 enum bool Pred (alias FR) = (searching == FR.Name); 967 } 968 969 /// Returns whether or not the field has a `enabled` / `disabled` field, 970 /// and its value. If it does not, returns `true`. 971 private EnabledState isMappingEnabled (M) (Node node, auto ref M default_) 972 { 973 import std.meta : Filter; 974 975 alias EMT = Filter!(NameIs!("enabled").Pred, FieldRefTuple!M); 976 alias DMT = Filter!(NameIs!("disabled").Pred, FieldRefTuple!M); 977 978 static if (EMT.length) 979 { 980 static assert (DMT.length == 0, 981 "`enabled` field `" ~ EMT[0].FieldName ~ 982 "` conflicts with `disabled` field `" ~ DMT[0].FieldName ~ "`"); 983 984 if (auto ptr = "enabled" in node) 985 return EnabledState(EnabledState.Field.Enabled, (*ptr).as!bool); 986 return EnabledState(EnabledState.Field.Enabled, __traits(getMember, default_, EMT[0].FieldName)); 987 } 988 else static if (DMT.length) 989 { 990 if (auto ptr = "disabled" in node) 991 return EnabledState(EnabledState.Field.Disabled, (*ptr).as!bool); 992 return EnabledState(EnabledState.Field.Disabled, __traits(getMember, default_, DMT[0].FieldName)); 993 } 994 else 995 { 996 return EnabledState(EnabledState.Field.None); 997 } 998 } 999 1000 /// Return value of `isMappingEnabled` 1001 private struct EnabledState 1002 { 1003 /// Used to determine which field controls a mapping enabled state 1004 private enum Field 1005 { 1006 /// No such field, the mapping is considered enabled 1007 None, 1008 /// The field is named 'enabled' 1009 Enabled, 1010 /// The field is named 'disabled' 1011 Disabled, 1012 } 1013 1014 /// Check if the mapping is considered enabled 1015 public bool opCast () const scope @safe pure @nogc nothrow 1016 { 1017 return this.field == Field.None || 1018 (this.field == Field.Enabled && this.fieldValue) || 1019 (this.field == Field.Disabled && !this.fieldValue); 1020 } 1021 1022 /// Type of field found 1023 private Field field; 1024 1025 /// Value of the field, interpretation depends on `field` 1026 private bool fieldValue; 1027 } 1028 1029 /// Evaluates to `true` if `T` is a `struct` with a default ctor 1030 private enum hasFieldwiseCtor (T) = (is(T == struct) && is(typeof(() => T(T.init.tupleof)))); 1031 1032 /// Evaluates to `true` if `T` has a static method that is designed to work with this library 1033 private enum hasFromYAML (T) = is(typeof(T.fromYAML(ConfigParser!(T).init)) : T); 1034 1035 /// Evaluates to `true` if `T` has a static method that accepts a `string` and returns a `T` 1036 private enum hasFromString (T) = is(typeof(T.fromString(string.init)) : T); 1037 1038 /// Evaluates to `true` if `T` is a `struct` which accepts a single string as argument 1039 private enum hasStringCtor (T) = (is(T == struct) && is(typeof(T.__ctor)) && 1040 Parameters!(T.__ctor).length == 1 && 1041 is(typeof(() => T(string.init)))); 1042 1043 unittest 1044 { 1045 static struct Simple 1046 { 1047 int value; 1048 string otherValue; 1049 } 1050 1051 static assert( hasFieldwiseCtor!Simple); 1052 static assert(!hasStringCtor!Simple); 1053 1054 static struct PubKey 1055 { 1056 ubyte[] data; 1057 1058 this (string hex) @safe pure nothrow @nogc{} 1059 } 1060 1061 static assert(!hasFieldwiseCtor!PubKey); 1062 static assert( hasStringCtor!PubKey); 1063 1064 static assert(!hasFieldwiseCtor!string); 1065 static assert(!hasFieldwiseCtor!int); 1066 static assert(!hasStringCtor!string); 1067 static assert(!hasStringCtor!int); 1068 } 1069 1070 /// Convenience function to extend a YAML path 1071 private string addPath (string opath, string newPart) 1072 in(newPart.length) 1073 do { 1074 return opath.length ? format("%s.%s", opath, newPart) : newPart; 1075 }