1 module dud.pkgdescription.json; 2 3 import std.algorithm.iteration : map, each, joiner, splitter; 4 import std.algorithm.mutation : copy; 5 import std.algorithm.searching : canFind, startsWith; 6 import std.array : array, empty, front, popFront, appender; 7 import std.conv : to; 8 import std.exception : enforce; 9 import std.format : format, formattedWrite; 10 import std.json; 11 import std.range : tee; 12 import std.stdio; 13 import std..string : indexOf; 14 import std.typecons : nullable, Nullable, tuple; 15 import std.traits : FieldNameTuple; 16 17 import dud.pkgdescription.exception; 18 import dud.pkgdescription.outpututils; 19 import dud.pkgdescription.udas; 20 import dud.pkgdescription; 21 import dud.semver.semver : SemVer; 22 import dud.semver.versionrange; 23 24 @safe pure: 25 26 // 27 // PackageDescription 28 // 29 30 PackageDescription jsonToPackageDescription(string js) { 31 import std.encoding : getBOM, BOM, BOMSeq; 32 immutable(BOMSeq) bom = () @trusted { 33 return getBOM(cast(ubyte[])js); 34 }(); 35 js = js[bom.sequence.length .. $]; 36 37 JSONValue jv = parseJSON(js); 38 return jGetPackageDescription(jv); 39 } 40 41 PackageDescription jsonToPackageDescription(JSONValue jv) { 42 return jGetPackageDescription(jv); 43 } 44 45 // 46 // Platform 47 // 48 49 Platform[] keyToPlatform(string key) { 50 auto s = key.startsWith("-") 51 ? key[1 .. $].splitter('-') 52 : key.splitter('-'); 53 enforce!EmptyInput(!s.empty, format("'%s' is an invalid key", key)); 54 s.popFront(); 55 return toPlatform(s); 56 } 57 58 Platform[] toPlatform(In)(ref In input) { 59 import std.algorithm.sorting : sort; 60 import std.algorithm : uniq; 61 62 return input 63 .map!(it => it == "unittest" 64 ? "unittest_" 65 : it == "assert" 66 ? "assert_" 67 : it 68 ) 69 .map!(it => to!Platform(it)) 70 .array 71 .sort 72 .uniq 73 .array; 74 } 75 76 void platformToS(Out)(auto ref Out o, const(Platform)[] p) { 77 p 78 .map!(it => it == Platform.unittest_ 79 ? "unittest" 80 : it == Platform.assert_ 81 ? "assert" 82 : to!string(it) 83 ) 84 .map!(s => to!string(s)) 85 .joiner("-") 86 .copy(o); 87 } 88 89 string platformKeyToS(string key, const(Platform)[] p) { 90 auto app = appender!string(); 91 formattedWrite(app, "%s", key); 92 if(!p.empty) { 93 app.put('-'); 94 platformToS(app, p); 95 } 96 return app.data; 97 } 98 99 Platform[][] jGetPlatforms(ref JSONValue jv) { 100 typeCheck(jv, [JSONType.array]); 101 102 return jv.arrayNoRef() 103 .tee!(it => typeCheck(it, [JSONType..string])) 104 .map!(it => it.str()) 105 .map!(it => it.splitter("-").map!(s => to!Platform(s)).array) 106 .array; 107 } 108 109 JSONValue platformsToJ(const Platform[][] plts) { 110 return plts.empty 111 ? JSONValue.init 112 : JSONValue( 113 plts 114 .map!(plt => plt 115 .map!(p => to!string(p)) 116 .joiner("-") 117 .array) 118 .array 119 ); 120 } 121 122 // 123 // SemVer 124 // 125 126 JSONValue semVerToJ(const SemVer v) { 127 return v == SemVer.init 128 ? JSONValue.init 129 : JSONValue(v.toString()); 130 } 131 132 SemVer jGetSemVer(ref JSONValue jv) { 133 import dud.semver.parse : parseSemVer; 134 string s = jGetString(jv); 135 return parseSemVer(s); 136 } 137 138 // 139 // string 140 // 141 142 string jGetString(ref JSONValue jv) { 143 typeCheck(jv, [JSONType..string]); 144 return jv.str(); 145 } 146 147 JSONValue stringToJ(const string s) { 148 return s.empty ? JSONValue.init : JSONValue(s); 149 } 150 151 // 152 // Strings, StringsPlatform 153 // 154 155 void jGetStringsPlatform(ref JSONValue jv, string key, ref Strings output) { 156 typeCheck(jv, [JSONType.array]); 157 158 StringsPlatform ret; 159 ret.strs = jGetStrings(jv); 160 ret.platforms = keyToPlatform(key); 161 162 output.platforms ~= ret; 163 } 164 165 void stringsPlatformToJ(const Strings s, const string key, 166 ref JSONValue output) 167 { 168 typeCheck(output, [JSONType.object, JSONType.null_]); 169 170 s.platforms.each!(delegate(const(StringsPlatform) it) pure @safe { 171 string nKey = platformKeyToS(key, it.platforms); 172 if(output.type == JSONType.object && nKey in output) { 173 throw new ConflictingOutput(format( 174 "'%s' already present in output JSON", nKey)); 175 } else if(output.type == JSONType.object && nKey !in output) { 176 output[nKey] = JSONValue(it.strs); 177 } else { 178 output = JSONValue([nKey : it.strs]); 179 } 180 }); 181 } 182 183 // 184 // String, StringPlatform 185 // 186 187 void jGetStringPlatform(ref JSONValue jv, string key, ref String output) { 188 typeCheck(jv, [JSONType..string]); 189 190 StringPlatform ret; 191 ret.str = jv.str(); 192 ret.platforms = keyToPlatform(key); 193 194 output.platforms ~= ret; 195 } 196 197 void stringPlatformToJ(const String s, const string key, ref JSONValue output) { 198 typeCheck(output, [JSONType.object, JSONType.null_]); 199 200 s.platforms.each!(it => output[platformKeyToS(key, it.platforms)] = 201 JSONValue(it.str)); 202 } 203 204 // 205 // strings 206 // 207 208 string[] jGetStrings(ref JSONValue jv) { 209 typeCheck(jv, [JSONType.array]); 210 return jv.arrayNoRef().map!(it => jGetString(it)).array; 211 } 212 213 JSONValue stringsToJ(const string[] ss) { 214 return ss.empty 215 ? JSONValue.init 216 : JSONValue(ss.map!(s => s).array); 217 } 218 219 // 220 // path 221 // 222 223 void jGetUnprocessedPath(ref JSONValue jv, string key, 224 ref UnprocessedPath output) 225 { 226 typeCheck(jv, [JSONType..string]); 227 228 output = UnprocessedPath(jv.str()); 229 } 230 231 void unprocessedPathToJ(const UnprocessedPath s, const string key, 232 ref JSONValue output) 233 { 234 typeCheck(output, [JSONType.object, JSONType.null_]); 235 236 if(!s.path.empty) { 237 output[key] = JSONValue(s.path); 238 } 239 } 240 241 void jGetPath(ref JSONValue jv, string key, ref Path output) { 242 typeCheck(jv, [JSONType..string]); 243 244 PathPlatform ret; 245 ret.path = UnprocessedPath(jv.str()); 246 ret.platforms = keyToPlatform(key); 247 output.platforms ~= ret; 248 } 249 250 void pathToJ(const Path s, const string key, ref JSONValue output) { 251 typeCheck(output, [JSONType.object, JSONType.null_]); 252 253 s.platforms.each!(it => output[platformKeyToS(key, it.platforms)] = 254 JSONValue(it.path.path)); 255 } 256 257 // 258 // paths 259 // 260 261 void jGetPaths(ref JSONValue jv, string key, ref Paths output) { 262 typeCheck(jv, [JSONType.array]); 263 264 PathsPlatform tmp; 265 tmp.platforms = keyToPlatform(key); 266 tmp.paths = jv.arrayNoRef() 267 .map!(j => j.str()) 268 .map!(s => UnprocessedPath(s)).array; 269 270 output.platforms ~= tmp; 271 } 272 273 void pathsToJ(const Paths ss, const string key, ref JSONValue output) { 274 typeCheck(output, [JSONType.object, JSONType.null_]); 275 276 ss.platforms 277 .each!(pp => 278 output[platformKeyToS(key, pp.platforms)] 279 = JSONValue(pp.paths.map!(p => p.path).array) 280 ); 281 } 282 283 // 284 // bool 285 // 286 287 bool jGetBool(ref JSONValue jv) { 288 typeCheck(jv, [JSONType.true_, JSONType.false_]); 289 return jv.boolean(); 290 } 291 292 // 293 // Dependency 294 // 295 296 void jGetDependencies(ref JSONValue jv, string key, ref Dependency[] deps) { 297 void insert(ref Dependency[string] ret, Dependency nd) pure { 298 ret[nd.name] = nd; 299 } 300 301 Dependency depFromJSON(T)(ref T it, Platform[] plts) pure { 302 Dependency t = extractDependency(it.value); 303 t.platforms = plts; 304 t.name = it.key; 305 return t; 306 } 307 308 Dependency extractDependencyStr(ref JSONValue jv) pure { 309 import dud.semver.versionrange; 310 311 typeCheck(jv, [JSONType..string]); 312 313 Dependency ret; 314 ret.version_ = parseVersionRange(jv.str()); 315 return ret; 316 } 317 318 Dependency extractDependencyObj(ref JSONValue jv) pure { 319 import dud.semver.versionrange; 320 321 typeCheck(jv, [JSONType.object]); 322 323 Dependency ret; 324 foreach(keyF, value; jv.objectNoRef()) { 325 switch(keyF) { 326 case "version": 327 ret.version_ = parseVersionRange(jGetString(value)); 328 break; 329 case "path": 330 ret.path.path = jGetString(value); 331 break; 332 case "optional": 333 ret.optional = nullable(jGetBool(value)); 334 break; 335 case "default": 336 ret.default_ = nullable(jGetBool(value)); 337 break; 338 default: 339 throw new Exception(format( 340 "Key '%s' is not part of a Dependency declaration", 341 keyF)); 342 } 343 } 344 345 return ret; 346 } 347 348 Dependency extractDependency(ref JSONValue jv) pure { 349 typeCheck(jv, [JSONType.object, JSONType..string]); 350 return jv.type == JSONType.object 351 ? extractDependencyObj(jv) 352 : extractDependencyStr(jv); 353 } 354 355 typeCheck(jv, [JSONType.object]); 356 357 const string noPlatform = splitOutKey(key); 358 Platform[] plts = keyToPlatform(key); 359 deps ~= jv.objectNoRef() 360 .byKeyValue() 361 .map!(it => depFromJSON(it, plts)) 362 .array; 363 } 364 365 void dependenciesToJ(const Dependency[] deps, string key, ref JSONValue jv) { 366 JSONValue[string][string] tmp; 367 deps.each!(dep => tmp[platformKeyToS(key, dep.platforms)][dep.name] = 368 dependencyToJ(dep)); 369 foreach(keyF, value; tmp) { 370 jv[keyF] = JSONValue(value); 371 } 372 } 373 374 JSONValue dependencyToJ(const Dependency dep) { 375 import dud.pkgdescription.helper; 376 377 bool isShortFrom(const Dependency d) pure { 378 return !d.version_.isNull() 379 && d.path.path.empty 380 && d.optional.isNull() 381 && d.default_.isNull(); 382 } 383 384 JSONValue ret; 385 if(isShortFrom(dep)) { 386 return JSONValue(dep.version_.get().toString()); 387 } 388 static foreach(mem; FieldNameTuple!Dependency) {{ 389 alias MemType = typeof(__traits(getMember, Dependency, mem)); 390 391 enum Mem = PreprocessKey!(mem); 392 393 static if(is(MemType == string)) {{ 394 // no need to handle this, this is stored as a json key 395 }} else static if(is(MemType == Nullable!VersionRange)) {{ 396 if(!__traits(getMember, dep, mem).isNull()) { 397 ret[Mem] = __traits(getMember, dep, mem).get().toString(); 398 } 399 }} else static if(is(MemType == UnprocessedPath)) {{ 400 const UnprocessedPath p = __traits(getMember, dep, mem); 401 if(!p.path.empty) { 402 ret[Mem] = p.path; 403 } 404 }} else static if(is(MemType == Nullable!bool)) {{ 405 if(!__traits(getMember, dep, mem).isNull()) { 406 ret[Mem] = __traits(getMember, dep, mem).get(); 407 } 408 }} else static if(is(MemType == Platform[])) {{ 409 // not handled here 410 }} else { 411 static assert(false, "Unhandeld type " ~ MemType.stringof ~ 412 " for mem " ~ Mem); 413 } 414 }} 415 return ret; 416 } 417 418 // 419 // SubPackage 420 // 421 422 SubPackage jGetSubpackageStr(ref JSONValue jv) { 423 SubPackage ret; 424 PathPlatform pp; 425 pp.path.path = jGetString(jv); 426 ret.path.platforms ~= pp; 427 return ret; 428 } 429 430 SubPackage jGetSubpackageObj(ref JSONValue jv) { 431 SubPackage ret; 432 ret.inlinePkg = jGetPackageDescription(jv); 433 return ret; 434 } 435 436 SubPackage jGetSubPackage(ref JSONValue jv) { 437 typeCheck(jv, [JSONType.object, JSONType..string]); 438 return jv.type == JSONType.object 439 ? jGetSubpackageObj(jv) 440 : jGetSubpackageStr(jv); 441 } 442 443 SubPackage[] jGetSubPackages(ref JSONValue jv) { 444 typeCheck(jv, [JSONType.array]); 445 return jv.arrayNoRef().map!(it => jGetSubPackage(it)).array; 446 } 447 448 JSONValue subPackagesToJ(const SubPackage[] sps) { 449 if(sps.empty) { 450 return JSONValue.init; 451 } 452 453 JSONValue[] ret; 454 foreach(sp; sps) { 455 if(!sp.inlinePkg.isNull()) { 456 ret ~= packageDescriptionToJ(sp.inlinePkg.get()); 457 } else { 458 enforce!EmptyInput(!sp.path.platforms.empty, 459 "SubPackage entry must be either Package description or path"); 460 ret ~= stringToJ(sp.path.platforms.front.path.path); 461 } 462 } 463 return JSONValue(ret); 464 } 465 466 // 467 // BuildType 468 // 469 470 void jGetBuildType(ref JSONValue jv, string key, ref BuildType bt) { 471 bt.name = key; 472 bt.pkg = jGetPackageDescription(jv); 473 } 474 475 void jGetBuildTypes(ref JSONValue jv, string key, ref BuildType[string] bts) { 476 typeCheck(jv, [JSONType.object]); 477 foreach(keyF, value; jv.objectNoRef()) { 478 BuildType tmp; 479 jGetBuildType(value, keyF, tmp); 480 bts[keyF] = tmp; 481 } 482 } 483 484 void buildTypesToJ(const BuildType[string] bts, const string key, 485 ref JSONValue ret) 486 { 487 typeCheck(ret, [JSONType.object, JSONType.null_]); 488 if(bts.empty) { 489 return; 490 } 491 492 JSONValue[string] map; 493 foreach(keyF, value; bts) { 494 JSONValue tmp = packageDescriptionToJ(value.pkg); 495 map[keyF] = tmp; 496 } 497 ret["buildTypes"] = map; 498 } 499 500 // 501 // BuildOption 502 // 503 504 void jGetBuildOptions(ref JSONValue jv, string key, ref BuildOptions bos) { 505 immutable(Platform[]) iPlts = keyToPlatform(key); 506 507 if(iPlts.empty) { 508 bos.unspecifiedPlatform = jv.arrayNoRef() 509 .map!(it => it.str()) 510 .map!(s => to!BuildOption(s)) 511 .array; 512 } else { 513 bos.platforms[iPlts] = jv.arrayNoRef() 514 .map!(it => it.str()) 515 .map!(s => to!BuildOption(s)) 516 .array; 517 } 518 } 519 520 void buildOptionsToJ(const BuildOptions bos, const string key, 521 ref JSONValue ret) 522 { 523 if(!bos.unspecifiedPlatform.empty) { 524 JSONValue j = JSONValue( 525 bos.unspecifiedPlatform.map!(bo => to!string(bo)).array); 526 ret["buildOptions"] = j; 527 } 528 529 foreach(plts, value; bos.platforms) { 530 ret[platformKeyToS("buildOptions", plts)] = 531 JSONValue(value.map!(bo => to!string(bo)).array); 532 } 533 } 534 535 // 536 // TargetType 537 // 538 539 TargetType jGetTargetType(ref JSONValue jv) { 540 import std.conv : to; 541 string s = jGetString(jv); 542 return to!TargetType(s); 543 } 544 545 JSONValue targetTypeToJ(const TargetType t) { 546 return t == TargetType.autodetect ? JSONValue.init : JSONValue(to!string(t)); 547 } 548 549 // 550 // PackageDescription 551 // 552 553 PackageDescription[string] jGetPackageDescriptions(JSONValue js) { 554 typeCheck(js, [JSONType.array]); 555 PackageDescription[string] ret; 556 js.arrayNoRef() 557 .each!((JSONValue it) { 558 PackageDescription tmp = jGetPackageDescription(it); 559 ret[tmp.name] = tmp; 560 }); 561 return ret; 562 } 563 564 template isPlatfromDependend(T) { 565 enum isPlatfromDependend = 566 is(T == String) 567 || is(T == Strings) 568 || is(T == Dependency[]) 569 || is(T == Path) 570 || is(T == UnprocessedPath) 571 || is(T == BuildRequirements) 572 || is(T == SubConfigs) 573 || is(T == BuildOptions) 574 || is(T == BuildType[string]) 575 || is(T == ToolchainRequirement[Toolchain]) 576 || is(T == Paths); 577 } 578 579 PackageDescription jGetPackageDescription(JSONValue js) { 580 typeCheck(js, [JSONType.object, JSONType.null_]); 581 582 PackageDescription ret; 583 if(js.type == JSONType.null_) { 584 return ret; 585 } 586 587 foreach(string key, ref JSONValue value; js.objectNoRef()) { 588 const string noPlatform = splitOutKey(key); 589 sw: switch(noPlatform) { 590 static foreach(mem; FieldNameTuple!PackageDescription) {{ 591 enum Mem = JSONName!mem; 592 alias get = JSONGet!mem; 593 alias MemType = typeof(__traits(getMember, ret, mem)); 594 case Mem: 595 static if(isPlatfromDependend!MemType) { 596 get(value, key, __traits(getMember, ret, mem)); 597 } else { 598 __traits(getMember, ret, mem) = get(value); 599 } 600 break sw; 601 }} 602 default: 603 throw new KeyNotHandled( 604 key == noPlatform 605 ? format("The json dud format does not know a key '%s'.", 606 key) 607 : format("The json dud format does not know a key '%s'." 608 ~ " Without platform '%s'", key, noPlatform) 609 ); 610 } 611 } 612 return ret; 613 } 614 615 JSONValue packageDescriptionToJ(const PackageDescription pkg) { 616 JSONValue ret; 617 static foreach(mem; FieldNameTuple!PackageDescription) {{ 618 enum Mem = JSONName!mem; 619 alias put = JSONPut!mem; 620 alias MemType = typeof(__traits(getMember, PackageDescription, mem)); 621 static if(is(MemType : Nullable!Args, Args...)) { 622 if(!__traits(getMember, pkg, mem).isNull()) { 623 JSONValue tmp = put(__traits(getMember, pkg, mem).get()); 624 if(tmp.type != JSONType.null_) { 625 ret[Mem] = tmp; 626 } 627 } 628 } else { 629 static if(isPlatfromDependend!MemType) { 630 put(__traits(getMember, pkg, mem), Mem, ret); 631 } else { 632 JSONValue tmp = put(__traits(getMember, pkg, mem)); 633 if(tmp.type != JSONType.null_) { 634 ret[Mem] = tmp; 635 } 636 } 637 } 638 }} 639 return ret; 640 } 641 642 JSONValue packageDescriptionsToJ(const PackageDescription[string] pkgs) { 643 return pkgs.empty 644 ? JSONValue.init 645 : JSONValue(pkgs.byValue().map!(it => packageDescriptionToJ(it)).array); 646 } 647 648 // 649 // BuildRequirement 650 // 651 652 BuildRequirement jGetBuildRequirement(ref JSONValue jv) { 653 string s = jGetString(jv); 654 return to!BuildRequirement(s); 655 } 656 657 void jGetBuildRequirements(ref JSONValue jv, string key, 658 ref BuildRequirements requirements) 659 { 660 typeCheck(jv, [JSONType.array]); 661 662 Platform[] platforms = keyToPlatform(key); 663 BuildRequirement[] tmp = jv.arrayNoRef() 664 .map!(it => jGetBuildRequirement(it)) 665 .array; 666 667 requirements.platforms ~= BuildRequirementPlatform(tmp, platforms); 668 } 669 670 void buildRequirementsToJ(const BuildRequirements brs, string key, 671 ref JSONValue ret) 672 { 673 typeCheck(ret, [JSONType.object, JSONType.null_]); 674 675 if(brs.platforms.empty) { 676 return; 677 } 678 679 brs.platforms 680 .each!(br => ret[platformKeyToS(key, br.platforms)] 681 = JSONValue(br.requirements.map!(it => to!string(it)).array)); 682 } 683 684 // 685 // string[string][Platform[]] 686 // 687 688 void jGetStringAA(ref JSONValue jv, string key, ref SubConfigs ret) { 689 typeCheck(jv, [JSONType.object]); 690 immutable(Platform[]) platforms = keyToPlatform(key); 691 692 string[string] tmp; 693 foreach(pkg, value; jv.objectNoRef()) { 694 if(platforms.empty) { 695 ret.unspecifiedPlatform[pkg] = value.str(); 696 } else { 697 tmp[pkg] = value.str(); 698 } 699 } 700 if(!platforms.empty) { 701 ret.configs[platforms] = tmp; 702 } 703 } 704 705 void stringAAToJ(const SubConfigs aa, const string key, ref JSONValue ret) { 706 JSONValue unspecific; 707 aa.unspecifiedPlatform.byKeyValue() 708 .each!(it => unspecific[it.key] = it.value); 709 710 if(!aa.unspecifiedPlatform.empty) { 711 ret["subConfigurations"] = unspecific; 712 } 713 714 foreach(plt, value; aa.configs) { 715 JSONValue tmp; 716 foreach(pkg, ver; value) { 717 tmp[pkg] =ver; 718 } 719 if(!value.empty) { 720 string k = platformKeyToS("subConfigurations", plt); 721 ret[k] = tmp; 722 } 723 } 724 } 725 726 // 727 // ToolchainRequirement 728 // 729 730 Toolchain jGetToolchain(string s) { 731 return to!Toolchain(s); 732 } 733 734 ToolchainRequirement jGetToolchainRequirement(ref JSONValue jv) { 735 typeCheck(jv, [JSONType..string]); 736 const string s = jv.str; 737 return s == "no" 738 ? ToolchainRequirement(true, VersionRange.init) 739 : ToolchainRequirement(false, parseVersionRange(s).get()); 740 } 741 742 void insertInto(const Toolchain tc, const ToolchainRequirement tcr, 743 ref ToolchainRequirement[Toolchain] ret) 744 { 745 import dud.pkgdescription.duplicate : dup; 746 enforce!ConflictingInput(tc !in ret, format( 747 "'%s' is already in '%s'", tc, ret)); 748 ret[tc] = dup(tcr); 749 } 750 751 void jGetToolchainRequirement(ref JSONValue jv, string key, 752 ref ToolchainRequirement[Toolchain] ret) 753 { 754 typeCheck(jv, [JSONType.object]); 755 jv.objectNoRef() 756 .byKeyValue() 757 .map!(it => tuple(it.key.jGetToolchain(), 758 jGetToolchainRequirement(it.value))) 759 .each!(tup => insertInto(tup[0], tup[1], ret)); 760 } 761 762 string toolchainToString(const ToolchainRequirement tcr) { 763 return tcr.no ? "no" : tcr.version_.toString(); 764 } 765 766 void toolchainRequirementToJ(const ToolchainRequirement[Toolchain] tcrs, 767 const string key, ref JSONValue ret) 768 { 769 if(tcrs.empty) { 770 return; 771 } 772 typeCheck(ret, [JSONType.object, JSONType.null_]); 773 774 JSONValue[string] map; 775 foreach(keyF, value; tcrs) { 776 map[to!string(keyF)] = toolchainToString(value); 777 } 778 ret["toolchainRequirements"] = map; 779 } 780 781 // 782 // Helper 783 // 784 785 void typeCheck(const JSONValue got, const JSONType[] exp, 786 const string file = __FILE__, const size_t line = __LINE__) 787 { 788 assert(!exp.empty); 789 if(!canFind(exp, got.type)) { 790 throw new WrongTypeJSON( 791 exp.length == 1 792 ? format("Expected a JSONValue of type '%s' but got '%s'", 793 exp.front, got.type) 794 : format("Expected a JSONValue of types [%(%s, %)]' but got '%s'", 795 exp, got.type), 796 file, line); 797 } 798 } 799 800 string splitOutKey(string key) { 801 const ptrdiff_t dash = key.indexOf('-', 1); 802 const string noPlatform = dash == -1 ? key : key[0 .. dash]; 803 return noPlatform; 804 } 805 806 unittest { 807 assert(splitOutKey("hello-posix") == "hello"); 808 assert(splitOutKey("-hello-posix") == "-hello"); 809 assert(splitOutKey("-hello") == "-hello"); 810 }