1 module render; 2 3 import boilerplate; 4 import config; 5 import reverseResponseCodes; 6 import route; 7 import SchemaLoader : SchemaLoader; 8 import std.algorithm; 9 import std.array; 10 import std.exception; 11 import std.file; 12 import std.format; 13 import std.path; 14 import std.range; 15 import std.stdio; 16 import std.string; 17 import std.typecons; 18 import std.uni; 19 import std.utf; 20 import types; 21 22 class Render 23 { 24 @(This.Default!(() => ["boilerplate"])) 25 string[] imports; 26 27 @(This.Default) 28 string[] types; 29 30 string modulePrefix; 31 32 string[string] redirected; 33 34 bool[string] typesBeingGenerated; 35 36 // for resolving references when inlining 37 Type[][string] schemas; 38 39 string renderObject(string key, const Type value, const SchemaConfig config, string description) 40 { 41 const name = key.keyToTypeName; 42 43 if (auto objectType = cast(ObjectType) value) 44 { 45 return renderStruct(name, objectType, config, description); 46 } 47 if (auto allOf = cast(AllOf) value) 48 { 49 Type loadSchema(string target) 50 { 51 const string relPath = target.until("#/").array.toUTF8; 52 const string schemaName = target.find("#/").drop("#/".length); 53 if (relPath.empty) 54 { 55 // try to find schema in own set. 56 assert(schemaName in this.schemas, format!"%s missing in %s"( 57 schemaName, this.schemas.keys)); 58 return this.schemas[schemaName].pickBestType; 59 } 60 // We don't usually look at reference targets, so 61 // we manually invoke the loader just this once. 62 const string path = value.source.dirName.buildNormalizedPath(relPath); 63 auto loader = new SchemaLoader; 64 auto file = loader.load(path); 65 assert(schemaName in file.schemas, format!"%s missing in %s"( 66 schemaName, file.schemas.keys)); 67 return file.schemas[schemaName]; 68 } 69 Type[] flattenAllOf(AllOf allOf) 70 { 71 Type[] result = null; 72 foreach (child; allOf.children) 73 { 74 if (auto nextAllOf = cast(AllOf) child) 75 { 76 result ~= flattenAllOf(nextAllOf); 77 continue; 78 } 79 if (auto refChild = cast(Reference) child) 80 { 81 auto schema = loadSchema(refChild.target); 82 if (auto nextAllOf = cast(AllOf) schema) { 83 result ~= flattenAllOf(nextAllOf); 84 continue; 85 } 86 } 87 result ~= child; 88 } 89 return result; 90 } 91 auto children = flattenAllOf(allOf); 92 auto refChildren = children.map!(a => cast(Reference) a).filter!"a".array; 93 auto objChildren = children.map!(a => cast(ObjectType) a).filter!"a".array; 94 95 if (children.length == refChildren.length + objChildren.length) 96 { 97 // generate object with all refs but one inlined 98 auto substitute = new ObjectType(null, null); 99 string extra = null; 100 101 substitute.setSource(value.source); 102 /** 103 * We can make exactly one ref child the alias-this. 104 * How do we pick? Easy: Use the one with the most properties. 105 */ 106 Reference refWithMostProperties = null; 107 // Resolve all references in the course of looking for the fattest child 108 Type[string] resolvedReferences; 109 110 foreach (ref child; children) 111 { 112 auto refChild = cast(Reference) child; 113 if (!refChild) 114 continue; 115 auto schema = loadSchema(refChild.target); 116 if (refChild.target.startsWith("#/")) 117 { 118 if (auto nextRef = cast(Reference) schema) 119 { 120 /** 121 * Reference in the same file that points at another reference? 122 * This must be the workaround for https://github.com/APIDevTools/swagger-cli/issues/59 123 * Bypass the first reference entirely. 124 */ 125 child = nextRef; 126 refChild = nextRef; 127 schema = loadSchema(nextRef.target); 128 } 129 } 130 resolvedReferences[refChild.target] = schema; 131 if (auto obj = cast(ObjectType) schema) 132 { 133 if (!refWithMostProperties) 134 { 135 refWithMostProperties = refChild; 136 } 137 else if (auto oldObj = cast(ObjectType) resolvedReferences[refWithMostProperties.target]) 138 { 139 if (obj.properties.length > oldObj.properties.length) 140 { 141 refWithMostProperties = refChild; 142 } 143 } 144 } 145 } 146 147 foreach (child; children) 148 { 149 if (auto obj = cast(ObjectType) child) 150 { 151 substitute.properties ~= obj.properties; 152 substitute.required ~= obj.required; 153 } 154 else if (auto refChild = cast(Reference) child) 155 { 156 if (refChild is refWithMostProperties) 157 continue; 158 if (!refChild.target.canFind("#/")) 159 { 160 stderr.writefln!"Don't understand reference target %s"(refChild.target); 161 assert(false); 162 } 163 164 auto schema = resolvedReferences[refChild.target]; 165 166 if (auto obj = cast(ObjectType) schema) 167 { 168 substitute.properties ~= obj.properties; 169 substitute.required ~= obj.required; 170 } 171 else 172 { 173 stderr.writefln!"Reference %s target %s isn't an object; cannot inline"( 174 refChild, schema); 175 assert(false); 176 } 177 } 178 else assert(false); 179 } 180 if (refWithMostProperties) 181 { 182 // use it for the alias-this reference 183 const fieldName = refWithMostProperties.target.keyToTypeName.asFieldName; 184 185 substitute.properties ~= TableEntry!Type(fieldName, refWithMostProperties); 186 substitute.required ~= fieldName; 187 extra = format!"alias %s this;"(fieldName); 188 } 189 return renderStruct(name, substitute, config, description, extra); 190 } 191 } 192 stderr.writefln!"ERR: not renderable %s; %s"(key, value.classinfo.name); 193 assert(false); 194 } 195 196 string renderStruct(string name, ObjectType objectType, const SchemaConfig config, string description, 197 string extra = null) 198 { 199 const(string)[] invariants = config.invariant_; 200 string result; 201 202 if (!description.empty) 203 { 204 result ~= description.renderComment(0, objectType.source); 205 } 206 result ~= format!"immutable struct %s\n{\n"(name); 207 string extraTypes, members; 208 foreach (tableEntry; objectType.properties) 209 { 210 const fieldName = tableEntry.key.fixReservedIdentifiers; 211 212 if (!config.properties.empty && !config.properties.canFind(fieldName)) 213 continue; 214 215 const required = objectType.required.canFind(tableEntry.key); 216 const optional = !required; 217 const allowNull = true; 218 219 members ~= renderMember(fieldName, tableEntry.value, 220 optional, allowNull, extraTypes); 221 members ~= "\n"; 222 } 223 if (!objectType.additionalProperties.isNull) 224 { 225 Type elementType = objectType.additionalProperties.get.type; 226 Nullable!int minProperties = objectType.additionalProperties.get.minProperties; 227 const optional = false, allowNull = true; 228 229 members ~= renderMember("additionalProperties", elementType, optional, allowNull, extraTypes, "[string]"); 230 members ~= "\n"; 231 if (!minProperties.isNull) 232 { 233 invariants ~= format!"this.additionalProperties.length >= %s"(minProperties.get); 234 } 235 } 236 237 result ~= extraTypes; 238 result ~= members; 239 foreach (invariant_; invariants) 240 { 241 result ~= format!" invariant (%s);\n\n"(invariant_); 242 } 243 if (!extra.empty) 244 { 245 result ~= format!" %s\n\n"(extra); 246 } 247 if (!objectType.required.filter!(a => config.properties.empty || config.properties.canFind(a)).empty) 248 { 249 // disabling this() on a struct with all-optional fields 250 // results in an unconstructable type 251 result ~= " @disable this();\n\n"; 252 } 253 if (!objectType.additionalProperties.isNull) 254 { 255 result ~= " alias additionalProperties this;\n\n"; 256 } 257 result ~= " mixin(GenerateAll);\n"; 258 result ~= "}\n"; 259 return result; 260 } 261 262 void renderEnum(string name, string[] members, string source, string description) 263 { 264 string result; 265 266 if (!description.empty) 267 { 268 result ~= description.renderComment(0, source); 269 } 270 result ~= format!"enum %s\n{\n"(name); 271 foreach (member; members) 272 { 273 result ~= " " ~ member.screamingSnakeToCamelCase.fixReservedIdentifiers ~ ",\n"; 274 } 275 result ~= "}\n"; 276 types ~= result; 277 } 278 279 void renderIdType(string name, string source, string description) 280 { 281 string result; 282 283 if (!description.empty) 284 { 285 result ~= description.renderComment(0, source); 286 } 287 result ~= format!"struct %s\n{\n"(name); 288 result ~= " import util.IdType : IdType;\n\n"; 289 result ~= format!" mixin IdType!%s;\n"(name); 290 result ~= "}\n"; 291 types ~= result; 292 } 293 294 string renderMember(string name, Type type, bool optional, bool allowNull, ref string extraTypes, 295 string modifier = "") 296 { 297 if (auto booleanType = cast(BooleanType) type) 298 { 299 if (optional) 300 { 301 assert(modifier == ""); 302 if (!booleanType.default_.isNull) 303 { 304 return format!" @(This.Default!%s)\n bool %s;\n"(booleanType.default_.get, name); 305 } 306 const fieldAllowNull = false; 307 return format!" @(This.Default)\n %s %s;\n"(nullableType("bool", "", fieldAllowNull), name); 308 } 309 return format!" bool%s %s;\n"(modifier, name); 310 } 311 string renderDType(string dType) 312 { 313 if (optional) 314 { 315 const nullableDType = nullableType(dType, modifier, allowNull); 316 317 return format!" @(This.Default)\n %s %s;\n"(nullableDType, name); 318 } 319 return format!" %s%s %s;\n"(dType, modifier, name); 320 } 321 if (auto numberType = cast(NumberType) type) 322 { 323 return renderDType("double"); 324 } 325 if (auto integerType = cast(IntegerType) type) 326 { 327 return renderDType(integerType.toDType); 328 } 329 if (auto stringType = cast(StringType) type) 330 { 331 string udaPrefix = ""; 332 if (!stringType.minLength.isNull && stringType.minLength.get == 1) 333 { 334 udaPrefix = " @NonEmpty\n"; 335 } 336 337 auto result = resolveSimpleStringType(stringType); 338 339 string actualType = "string"; 340 if (!result.isNull) 341 { 342 actualType = result.get.typeName; 343 imports ~= result.get.imports; 344 } 345 else if (!stringType.enum_.empty) 346 { 347 actualType = name.capitalize; 348 extraTypes ~= format!" enum %s\n {\n"(actualType); 349 foreach (member; stringType.enum_) 350 { 351 extraTypes ~= " " ~ member.screamingSnakeToCamelCase.fixReservedIdentifiers ~ ",\n"; 352 } 353 extraTypes ~= " }\n\n"; 354 } 355 356 if (optional) 357 { 358 return format!"%s @(This.Default)\n %s %s;\n"( 359 udaPrefix, nullableType(actualType, modifier, allowNull), name); 360 } 361 return format!"%s %s%s %s;\n"(udaPrefix, actualType, modifier, name); 362 } 363 if (auto objectType = cast(ObjectType) type) 364 { 365 if (objectType.properties.empty) 366 { 367 // string[string] object 368 if (objectType.additionalProperties.apply!(a => cast(StringType) a.type !is null).get(false)) 369 { 370 string prefix = null; 371 string typeStr = "string[string]"; 372 if (optional) 373 { 374 prefix ~= " @(This.Default)\n"; 375 typeStr = nullableType("string", "[string]", allowNull); 376 } 377 if (objectType.additionalProperties.get.minProperties.apply!(a => a == 1).get(false)) 378 { 379 prefix ~= " @NonEmpty\n"; 380 } 381 return format!"%s %s%s %s;\n"(prefix, typeStr, modifier, name); 382 } 383 imports ~= "std.json"; 384 if (optional) 385 { 386 return format!" @(This.Default)\n %s %s;\n"( 387 nullableType("JSONValue", modifier, allowNull), name); 388 } 389 return format!" JSONValue%s %s;\n"(modifier, name); 390 } 391 } 392 if (auto arrayType = cast(ArrayType) type) 393 { 394 // if we want an invariant, we must allow Nullable. 395 const allowElementNull = arrayType.minItems.isNull; 396 const member = renderMember(name, arrayType.items, optional, allowElementNull, extraTypes, modifier ~ "[]"); 397 398 if (!arrayType.minItems.isNull) 399 { 400 if (arrayType.minItems.get == 1) 401 { 402 return " @NonEmpty\n" ~ member; 403 } 404 if (arrayType.minItems.get > 1) 405 { 406 return member ~ format!"\n invariant (this.%s.length >= %s);\n"(name, arrayType.minItems.get); 407 } 408 } 409 return member; 410 } 411 if (auto reference = cast(Reference) type) 412 { 413 string tryInline() 414 { 415 if (!reference.target.canFind("#/")) return null; 416 417 const targetSchema = reference.target.find("#/").drop("#/".length); 418 419 if (targetSchema !in this.schemas) return null; 420 421 const typeName = reference.target.keyToTypeName; 422 423 if (!matchingImports(typeName).empty) 424 { 425 return null; 426 } 427 428 auto schema = this.schemas[targetSchema].pickBestType; 429 string inlineModifier = modifier; 430 // TODO factor out into helper (compare app.d toplevel simple-schema resolution) 431 while (auto arrayType = cast(ArrayType) schema) 432 { 433 schema = arrayType.items; 434 inlineModifier ~= "[]"; 435 } 436 if (auto stringType = cast(StringType) schema) 437 { 438 if (!stringType.enum_.empty || typeName.endsWith("Id")) return null; 439 } 440 else if (cast(BooleanType) schema || cast(NumberType) schema || cast(IntegerType) schema) 441 { 442 // inline alias 443 } 444 else 445 { 446 return null; 447 } 448 449 // inline alias 450 return renderMember(name, schema, optional, allowNull, extraTypes, inlineModifier); 451 } 452 453 if (auto result = tryInline) 454 { 455 return result; 456 } 457 458 const result = resolveReference(reference); 459 460 if (!result.import_.isNull) 461 { 462 imports ~= result.import_.get; 463 } 464 const typeName = result.typeName; 465 466 if (optional) 467 { 468 return format!" @(This.Default)\n %s %s;\n"(nullableType(typeName, modifier, allowNull), name); 469 } 470 return format!" %s%s %s;\n"(typeName, modifier, name); 471 } 472 473 // render as subtype 474 const capitalizedName = name.capitalizeFirst; 475 const typeName = modifier.isArrayModifier ? capitalizedName.singularize : capitalizedName; 476 477 extraTypes ~= renderObject(typeName, type, SchemaConfig(), null).indent ~ "\n"; 478 if (optional) 479 { 480 return format!" @(This.Default)\n %s %s;\n"(nullableType(typeName, modifier, allowNull), name); 481 } 482 return format!" %s%s %s;\n"(typeName, modifier, name); 483 } 484 485 string renderRoutes(string name, string source, string description, const Route[] routes, 486 const Parameter[string] parameterComponents) 487 { 488 string[] lines; 489 string[] extraTypes; 490 491 imports ~= "messaging.Context : Context"; 492 imports ~= "net.http.ResponseCode"; 493 imports ~= "net.rest.Method"; 494 495 lines ~= "/**"; 496 lines ~= linebreak(" * ", " * ", ["This boundary interface has been generated from ", source ~ ":"]); 497 lines ~= description.split("\n").map!strip.strip!(a => a.empty).map!(a => stripRight(" * " ~ a)).array; 498 lines ~= " */"; 499 lines ~= format!"interface %s"(name); 500 lines ~= "{"; 501 foreach (i, route; routes) 502 { 503 const availableParameters = route.parameters 504 .map!(a => resolveParameter(a, parameterComponents)) 505 .filter!(a => a.in_ == "path") 506 .array; 507 508 const(ValueParameter) findParameterWithName(string name) 509 { 510 enforce(availableParameters.any!(a => a.name == name), 511 format!"route parameter with name \"%s\" not found"(name)); 512 return availableParameters.find!(a => a.name == name).front; 513 } 514 515 const urlParameters = route.url.split("/") 516 .filter!(a => a.startsWith("{") && a.endsWith("}")) 517 .map!(name => findParameterWithName(name.dropOne.dropBackOne)) 518 .array; 519 string[] dParameters = null; 520 521 void addDParameter(const Type type, const string name, bool required = true) 522 { 523 if (!required) 524 { 525 imports ~= "std.typecons"; 526 } 527 if (auto refType = cast(Reference) type) 528 { 529 const result = resolveReference(refType); 530 const typeName = required ? result.typeName : format!"Nullable!%s"(result.typeName); 531 532 if (!result.import_.isNull) 533 { 534 imports ~= result.import_.get(null); 535 } 536 dParameters ~= format!"const %s %s"(typeName, name); 537 } 538 else if (auto strType = cast(StringType) type) 539 { 540 auto stringType = "string"; 541 const result = resolveSimpleStringType(strType); 542 if (!result.isNull) 543 { 544 stringType = result.get.typeName; 545 imports ~= result.get.imports; 546 } 547 dParameters ~= format!"const %s%s %s"(required ? "" : "Nullable!", stringType, name); 548 } 549 else 550 { 551 assert(false, format!"Type currently unsupported for URL parameters: %s"(type)); 552 } 553 } 554 555 foreach (urlParameter; urlParameters) 556 { 557 addDParameter(urlParameter.schema, urlParameter.name); 558 } 559 560 string typeToString(Type type) 561 { 562 if (type is null) return "void"; 563 if (auto refType = cast(Reference) type) 564 { 565 const result = resolveReference(refType); 566 567 if (!result.import_.isNull) 568 { 569 imports ~= result.import_.get; 570 } 571 return result.typeName; 572 } 573 if (auto integerType = cast(IntegerType) type) 574 { 575 return integerType.toDType; 576 } 577 if (auto stringType = cast(StringType) type) 578 { 579 if (stringType.enum_.empty && stringType.format_.isNull) 580 { 581 return "string"; 582 } 583 } 584 if (auto arrayType = cast(ArrayType) type) 585 { 586 return typeToString(arrayType.items) ~ "[]"; 587 } 588 const bodyType = route.operationId.capitalizeFirst; 589 590 extraTypes ~= "\n" ~ renderObject(bodyType, type, SchemaConfig(), null); 591 return bodyType; 592 } 593 594 const string bodyType = typeToString(cast() route.schema); 595 596 if (bodyType != "void") 597 { 598 dParameters ~= "const " ~ bodyType; 599 } 600 601 const queryParameters = route.parameters 602 .map!(a => cast(ValueParameter) a) 603 .filter!"a !is null" 604 .filter!(a => a.in_ == "query") 605 .array; 606 607 foreach (queryParameter; queryParameters) 608 { 609 addDParameter(queryParameter.schema, queryParameter.name, queryParameter.required.get(false)); 610 } 611 612 string urlWithQueryParams = route.url; 613 614 if (!queryParameters.empty) 615 { 616 urlWithQueryParams ~= "?" ~ queryParameters.map!(a => format!"%s={%s}"(a.name, a.name)).join("&"); 617 } 618 619 if (i > 0) lines ~= ""; 620 lines ~= route.description.strip.split("\n").strip!(a => a.empty).renderComment(4); 621 lines ~= linebreak((4).spaces, (12).spaces, [ 622 format!"@(Method.%s!("(route.method.capitalizeFirst), 623 "JsonFormat, ", 624 format!"%s, "(route.schema ? bodyType : "void"), 625 format!"\"%s\"))"(urlWithQueryParams), 626 ]); 627 Type responseType = null; 628 foreach (response; route.responses) 629 { 630 if (response.code.startsWith("2")) { 631 assert(responseType is null); 632 responseType = cast() response.schema; 633 continue; 634 } 635 // produced by the networking lib 636 if (response.code == "422") continue; 637 638 enforce(response.schema is null, "Error response cannot return body"); 639 640 const member = codeToMember(response.code); 641 const exception = pickException(response.code); 642 643 lines ~= format!" @(Throws!(%s, ResponseCode.%s.expand))"(exception, member); 644 } 645 const string returnType = typeToString(responseType); 646 647 lines ~= linebreak((4).spaces, (8).spaces, [ 648 format!"public %s %s("(returnType, route.operationId), 649 ] ~ dParameters.map!(a => a ~ ", ").array ~ [ 650 "const Context context);" 651 ]); 652 } 653 lines ~= "}"; 654 // retro to counteract retro in app.d (sorry) 655 types ~= extraTypes.retro.array; 656 return lines.join("\n") ~ "\n"; 657 } 658 659 private string pickException(string responseCode) 660 { 661 string from(string package_, string member) 662 { 663 imports ~= package_; 664 return member; 665 } 666 switch (responseCode) 667 { 668 case "404": return from("util.NoSuchElementException", "NoSuchElementException"); 669 case "409": return from("util.IllegalArgumentException", "IllegalArgumentException"); 670 case "422": return from("util.IllegalArgumentException", "IllegalArgumentException"); 671 case "503": return from("util.ServiceUnavailableException", "ServiceUnavailableException"); 672 case "512": return from("util.ConcurrentModificationException", "ConcurrentModificationException"); 673 default: return from("std.exception", "Exception"); 674 } 675 } 676 677 Tuple!(string, "typeName", Nullable!string, "import_") resolveReference(const Reference reference) 678 { 679 const typeName = reference.target.keyToTypeName; 680 if (typeName in this.redirected) 681 { 682 return typeof(return)(typeName, this.redirected[typeName].nullable); 683 } 684 if (typeName in this.typesBeingGenerated) 685 { 686 return typeof(return)(typeName, Nullable!string(this.modulePrefix ~ "." ~ typeName)); 687 } 688 return .resolveReference(reference); 689 } 690 691 private string nullableType(string type, string modifier, bool allowNullInit) 692 { 693 if (allowNullInit && modifier.isArrayModifier) 694 { 695 // we can just use the type itself as the nullable type 696 return type ~ modifier; 697 } 698 imports ~= "std.typecons"; 699 if (modifier.empty) 700 { 701 return format!"Nullable!%s"(type); 702 } 703 return format!"Nullable!(%s%s)"(type, modifier); 704 } 705 706 mixin(GenerateThis); 707 } 708 709 Nullable!(Tuple!(string, "typeName", string[], "imports")) resolveSimpleStringType(const StringType type) 710 { 711 if (!type.enum_.empty) 712 { 713 return typeof(return)(); 714 } 715 if (type.format_ == "date-time") 716 { 717 return tuple!("typeName", "imports")("SysTime", ["std.datetime"]).nullable; 718 } 719 if (type.format_ == "date") 720 { 721 return tuple!("typeName", "imports")("Date", ["std.datetime"]).nullable; 722 } 723 if (type.format_ == "duration") 724 { 725 return tuple!("typeName", "imports")("Duration", ["std.datetime"]).nullable; 726 } 727 return typeof(return)(); 728 } 729 730 // If we have both a type definition for X and a link to X in another yml, 731 // then ignore the reference declarations. 732 Type pickBestType(Type[] list) 733 { 734 auto nonReference = list.filter!(a => !cast(Reference) a); 735 736 if (!nonReference.empty) 737 { 738 return nonReference.front; 739 } 740 return list.front; 741 } 742 743 // Given a list of fragments, linebreak and indent them to avoid exceeding 120 columns per line. 744 private string[] linebreak(string firstLineIndent, string restLineIndent, string[] fragments) 745 { 746 string[] lines = null; 747 string line = null; 748 749 string lineIndent() { return lines.empty ? firstLineIndent : restLineIndent; } 750 751 void flush() 752 { 753 if (line.empty) return; 754 lines ~= (lineIndent ~ line.stripLeft).stripRight; 755 line = null; 756 } 757 758 foreach (fragment; fragments) 759 { 760 if (lineIndent.length + line.length + fragment.length > 120) flush; 761 line ~= fragment; 762 } 763 flush; 764 return lines; 765 } 766 767 private const(ValueParameter) resolveParameter(const Parameter param, const Parameter[string] components) 768 { 769 if (auto reference = cast(RefParameter) param) 770 { 771 enforce(reference.target.startsWith("#/"), format!"cannot resolve indirect $ref parameter (TODO) \"%s\""( 772 reference.target)); 773 774 const target = reference.target["#/".length .. $]; 775 776 enforce(target in components, 777 format!"cannot find target for $ref parameter \"%s\""(reference.target)); 778 return components[target].resolveParameter(components); 779 } 780 if (auto valParameter = cast(ValueParameter) param) 781 { 782 return valParameter; 783 } 784 assert(false, format!"Unknown parameter type %s"(param.classinfo.name)); 785 } 786 787 alias Resolution = Tuple!(string, "typeName", Nullable!string, "import_"); 788 789 __gshared const(string)[] allFiles; 790 __gshared const(string)[][string] moduleCache; 791 __gshared Object cacheLock; 792 793 shared static this() 794 { 795 cacheLock = new Object; 796 allFiles = dirEntries("src", "*.d", SpanMode.depth) 797 .chain(dirEntries("include", "*.d", SpanMode.depth)) 798 .filter!(file => !file.name.endsWith("Test.d")) 799 .map!(a => a.readText) 800 .array; 801 } 802 803 private Resolution resolveReference(const Reference reference) 804 { 805 const typeName = reference.target.keyToTypeName; 806 const matchingImports = .matchingImports(typeName); 807 808 if (matchingImports.empty) 809 { 810 stderr.writefln!"WARN: no import found for type %s"(reference.target); 811 } 812 813 if (matchingImports.length > 1) 814 { 815 stderr.writefln!"WARN: multiple module sources for %s: %s, using %s"( 816 reference.target, matchingImports, matchingImports.front); 817 } 818 819 return Resolution(typeName, matchingImports.empty ? Nullable!string() : matchingImports.front.nullable); 820 } 821 822 private const(string)[] matchingImports(const string typeName) 823 { 824 synchronized (cacheLock) 825 { 826 if (auto ptr = typeName in moduleCache) 827 { 828 return *ptr; 829 } 830 831 const matches = allFiles 832 .filter!(a => a.canFind(format!"struct %s\n"(typeName)) 833 || a.canFind(format!"enum %s\n"(typeName))) 834 .map!(a => a.find("module ").drop("module ".length).until(";").toUTF8) 835 .array; 836 837 moduleCache[typeName] = matches; 838 return matches; 839 } 840 } 841 842 private string renderComment(string comment, int indent, string source) 843 { 844 const lines = [format!"This value object has been generated from %s:"(source)] ~ comment 845 .strip 846 .split("\n") 847 .strip!(a => a.empty); 848 849 return renderComment(lines, indent).join("\n") ~ "\n"; 850 } 851 852 private string[] renderComment(const string[] lines, int indent) 853 { 854 const spacer = ' '.repeat(indent).array; 855 856 return [format!"%s/**"(spacer)] 857 ~ lines.map!(line => format!"%s * %s"(spacer, line).stripRight).array 858 ~ format!"%s */"(spacer); 859 } 860 861 private bool isArrayModifier(string modifier) 862 { 863 return modifier.endsWith("[]"); 864 } 865 866 public alias keyToTypeName = target => target.split("/").back; 867 868 public alias asFieldName = type => chain(type.front.toLower.only, type.dropOne).toUTF8; 869 870 unittest 871 { 872 assert("Foo".asFieldName == "foo"); 873 assert("FooBar".asFieldName == "fooBar"); 874 } 875 876 private alias screamingSnakeToCamelCase = a => a 877 .split("_") 878 .map!toLower 879 .capitalizeAllButFirst 880 .join; 881 882 unittest 883 { 884 assert("FOO".screamingSnakeToCamelCase == "foo"); 885 assert("FOO_BAR".screamingSnakeToCamelCase == "fooBar"); 886 } 887 888 private alias capitalizeAllButFirst = range => chain(range.front.only, range.drop(1).map!capitalize); 889 890 private alias capitalizeFirst = range => chain(range.front.toUpper.only, range.drop(1)).toUTF8; 891 892 // Quick and dirty plural to singular conversion. 893 private string singularize(string name) 894 { 895 if (name.endsWith("s")) 896 { 897 return name.dropBack(1); 898 } 899 return name; 900 } 901 902 private string fixReservedIdentifiers(string name) 903 { 904 switch (name) 905 { 906 static foreach (identifier; reservedIdentifiers) 907 { 908 case identifier: 909 return identifier ~ "_"; 910 } 911 default: 912 return name; 913 } 914 } 915 916 private enum reservedIdentifiers = [ 917 "abstract", "alias", "align", "asm", "assert", "auto", 918 "body", "bool", "break", "byte", 919 "case", "cast", "catch", "cdouble", "cent", "cfloat", "char", "class", "const", "continue", "creal", 920 "dchar", "debug", "default", "delegate", "delete", "deprecated", "do", "double", 921 "else", "enum", "export", "extern", 922 "false", "final", "finally", "float", "for", "foreach", "foreach_reverse", "function", 923 "goto", 924 "idouble", "if", "ifloat", "immutable", "import", "in", "inout", "int", "interface", "invariant", "ireal", "is", 925 "lazy", "long", 926 "macro", "mixin", "module", 927 "new", "nothrow", "null", 928 "out", "override", 929 "package", "pragma", "private", "protected", "public", "pure", 930 "real", "ref", "return", 931 "scope", "shared", "short", "static", "struct", "super", "switch", "synchronized", 932 "template", "this", "throw", "true", "try", "typeid", "typeof", 933 "ubyte", "ucent", "uint", "ulong", "union", "unittest", "ushort", 934 "version", "void", 935 "wchar", "while", "with", 936 ]; 937 938 private string indent(string text) 939 { 940 string indentLine(string line) 941 { 942 return (4).spaces ~ line; 943 } 944 945 return text 946 .split("\n") 947 .map!(a => a.empty ? a : indentLine(a)) 948 .join("\n"); 949 } 950 951 private alias spaces = i => ' '.repeat(i).array.idup;