1 /// 2 module jaster.serialise.builder; 3 4 version(Jasterialise_Unittests) import fluent.asserts; 5 6 public import std.typecons : Flag, Yes, No; 7 8 /// Passed to functions such as `CodeBuilder.put` to control automatic tabbing. 9 alias UseTabs = Flag!"tabs"; 10 11 /// Passed to functions such as `CodeBuilder.put` to control automatic new lines. 12 alias UseNewLines = Flag!"newLines"; 13 14 /// A delegate that can be passed to functions such as `addFuncDeclaration` to allow flexible 15 /// generation of code. 16 alias CodeFunc = void delegate(CodeBuilder); 17 18 /++ 19 + A glorified wrapper around an `Appender` which supports automatic tabbing and new lines. 20 + 21 + On it's own, `CodeBuilder` may already be more desirable than manually formatting, tabbing new lining, 22 + a code string manually. 23 + 24 + UFCS can also be used to create functions that can ease the generation of code, such as `addFuncCall`, 25 + `addFuncDeclaration`, `addReturn`, etc. 26 ++/ 27 final class CodeBuilder 28 { 29 import std.array : Appender; 30 31 private 32 { 33 Appender!(dchar[]) _data; 34 size_t _tabs; 35 static const dchar _tabChar = '\t'; 36 37 size_t _disableTabCount; 38 size_t _disableLinesCount; 39 } 40 41 public 42 { 43 /++ 44 + Increases the tab count, meaning anytime `CodeBuilder.put` is used an extra tab will be written before 45 + the data passed to it. 46 + 47 + Notes: 48 + If the tab count is the same value as `size_t.max` then this function does nothing. 49 + ++/ 50 @safe @nogc 51 void entab() nothrow pure 52 { 53 // Overflow protection 54 if(this._tabs == size_t.max) 55 return; 56 57 this._tabs += 1; 58 } 59 60 /++ 61 + Decreases the tab count. 62 + 63 + Notes: 64 + If the tab count is 0 then this function does nothing. 65 + ++/ 66 @safe @nogc 67 void detab() nothrow pure 68 { 69 // Underflow protection 70 if(this._tabs == 0) 71 return; 72 73 this._tabs -= 1; 74 } 75 76 /++ 77 + Disables automatic tabbing and/or new line insertion. 78 + 79 + Notes: 80 + For every call to `disable`, a call to `enable` is required to re-enable the functionality. 81 + 82 + For example, if 2 calls to `disable` are made to disable tabbing, then 2 calls to `enable` for tabbing must be made 83 + before tabbing is re-enabled. 84 + 85 + Params: 86 + disableTabs = If `Yes.tabs` then automatic tabbing will be disabled. 87 + disableLines = If `Yes.newLines` then automatic new line insertion will be disabled. 88 + 89 + See_Also: 90 + `CodeBuilder.enable` 91 + ++/ 92 @safe @nogc 93 void disable(UseTabs disableTabs = Yes.tabs, UseNewLines disableLines = Yes.newLines) nothrow pure 94 { 95 void _disable(ref size_t counter, bool doAction) 96 { 97 if(counter == size_t.max || !doAction) 98 return; 99 100 counter += 1; 101 } 102 103 _disable(this._disableLinesCount, disableLines); 104 _disable(this._disableTabCount, disableTabs); 105 } 106 107 /++ 108 + Enables automatic tabbing and/or new line insertion. 109 + 110 + Params: 111 + enableTabs = If `Yes.tabs` then automatic tabbing will be enabled. 112 + enableLines = If `Yes.newLines` then automatic new line insertion will be enabled. 113 + 114 + See_Also: 115 + `CodeBuilder.disable` 116 + ++/ 117 @safe @nogc 118 void enable(UseTabs enableTabs = Yes.tabs, UseNewLines enableLines = Yes.newLines) nothrow pure 119 { 120 void _enable(ref size_t counter, bool doAction) 121 { 122 if(counter == 0 || !doAction) 123 return; 124 125 counter -= 1; 126 } 127 128 _enable(this._disableLinesCount, enableLines); 129 _enable(this._disableTabCount, enableTabs); 130 } 131 132 /++ 133 + Inserts data into the code string. 134 + 135 + Notes: 136 + `T` can be anything supported by `Appender!(dchar[])` 137 + 138 + `CodeBuilder.enable` and `CodeBuilder.disable` are used to enable/disable the functionality of 139 + `doTabs` and `doLines` regardless of their values. 140 + 141 + For ranges of `dchar[]` (such as `dchar[][]`) the functionality of `doTabs` and `doLines` will be applied to each 142 + `dchar[]` given. 143 + 144 + Params: 145 + data = The data to insert. 146 + doTabs = If `Yes.tabs` then a certain amount of tabs (see `CodeBuilder.entab`) will be inserted 147 + before `data` is inserted. 148 + doLines = If `Yes.newLines` then a new line will be inserted after `data`. 149 + ++/ 150 void put(T)(T data, UseTabs doTabs = Yes.tabs, UseNewLines doLines = Yes.newLines) 151 { 152 import std.array; 153 import std.algorithm : map; 154 import std.range : repeat, isInputRange, chain, ElementEncodingType, take; 155 156 // For now, I'm just going to rely on the compiler's error message for when 157 // the user passes something that Appender doesn't like. 158 159 if(this._disableLinesCount > 0) 160 doLines = No.newLines; 161 162 if(this._disableTabCount > 0) 163 doTabs = No.tabs; 164 165 auto tabs = this._tabChar.repeat((doTabs) ? this._tabs : 0); 166 auto line = ['\n'].take((doLines) ? 1 : 0); 167 168 static if(isInputRange!T && is(ElementEncodingType!T : dchar[])) // ranges of dchar[] 169 { 170 // TODO: Actually bother to test this. `chain` wouldn't work in the else statement, so possibly won't work here. 171 this._data.put(data.map!(str => chain(tabs, str, line))); 172 } 173 else // dstring/ranges of dchar 174 { 175 this._data.put(tabs); 176 this._data.put(data); 177 this._data.put(line); 178 } 179 } 180 181 /// overload ~= 182 void opOpAssign(string op : "~", T)(T data) 183 { 184 this.put(data); 185 } 186 187 /++ 188 + Returns: 189 + The code currently generated. 190 + ++/ 191 @property @safe @nogc 192 const(dchar)[] data() nothrow pure const 193 { 194 return this._data.data; 195 } 196 } 197 } 198 199 /// Describes a variable. 200 struct Variable 201 { 202 /// The name of the variable's type. 203 dstring typeName; 204 205 /// The name of the variable. 206 dstring name; 207 208 /// The `CodeFunc` which generates the default value of the variable. 209 CodeFunc defaultValue; 210 } 211 212 /++ 213 + A helper function that entabs the given `CodeBuilder`, calls a delegate to generate some code, and then detabs the `CodeBuilder`. 214 + 215 + Params: 216 + builder = The `CodeBuilder` to use. 217 + code = The `CodeFunc` to use. 218 + 219 + Returns: 220 + `builder` 221 + ++/ 222 CodeBuilder putEntabbed(CodeBuilder builder, CodeFunc code) 223 { 224 builder.entab(); 225 code(builder); 226 builder.detab(); 227 228 return builder; 229 } 230 /// 231 version(Jasterialise_Unittests) 232 unittest 233 { 234 auto builder = new CodeBuilder(); 235 236 builder.put("Hello"); 237 builder.data.should.equal("Hello\n"); 238 239 builder.putEntabbed(b => b.put("World")); 240 builder.data.should.equal("Hello\n\tWorld\n"); 241 } 242 243 /++ 244 + A helper function to write the given code in between two '"'s 245 + 246 + Notes: 247 + `T` can be any type that can be passed to `CodeBuilder.put`. 248 + 249 + Params: 250 + builder = The `CodeBuilder` to use. 251 + str = The code to write. 252 + 253 + Returns: 254 + `builder` 255 + ++/ 256 CodeBuilder putString(T)(CodeBuilder builder, T str) 257 { 258 builder.disable(); 259 260 builder.put('"'); 261 builder.put(str); 262 builder.put('"'); 263 264 builder.enable(); 265 return builder; 266 } 267 /// 268 version(Jasterialise_Unittests) 269 unittest 270 { 271 auto builder = new CodeBuilder(); 272 273 builder.put("Hello"); 274 builder.putString("World!"); 275 276 builder.data.should.equal("Hello\n\"World!\""); 277 } 278 279 /++ 280 + 281 + ++/ 282 CodeBuilder putScope(CodeBuilder builder, CodeFunc func) 283 { 284 builder.put('{'); 285 builder.putEntabbed(b => func(b)); 286 builder.put('}'); 287 288 return builder; 289 } 290 /// 291 version(Jasterialise_Unittests) 292 unittest 293 { 294 auto builder = new CodeBuilder(); 295 builder.putScope( 296 (b) 297 { 298 b.addFuncCall("writeln", "\"Hello world!\""); 299 }); 300 301 builder.data.should.equal("{\n\twriteln(\"Hello world!\");\n}\n"); 302 } 303 304 /++ 305 + Formatted version of `CodeBuilder.put`. 306 + 307 + Params: 308 + builder = The `CodeBuilder` to use. 309 + formatStr = The format string to pass to `std.format.format` 310 + params = The paramters to pass to `std.format.format` 311 + 312 + Returns: 313 + `builder` 314 + ++/ 315 CodeBuilder putf(Params...)(CodeBuilder builder, dstring formatStr, Params params) 316 { 317 import std.format : format; 318 319 builder.put(format(formatStr, params)); 320 321 return builder; 322 } 323 /// 324 version(Jasterialise_Unittests) 325 unittest 326 { 327 auto builder = new CodeBuilder(); 328 builder.putf("if(%s == %s)", "\"Hello\"", "\"World\""); 329 330 builder.data.should.equal("if(\"Hello\" == \"World\")\n"); 331 } 332 333 /++ 334 + A helper function which accepts a wide variety of parameters to pass to `CodeBuilder.put`. 335 + 336 + Supported_Types: 337 + InputRanges of characters (dstring, for example) - Written in as-is, with no modification. 338 + 339 + `CodeFunc` - The `CodeFunc` is called with `builder` as it's parameter. 340 + 341 + `Variable` - The name of the variable is written. 342 + 343 + Any built-in D type - The result of passing the parameter to `std.conv.to!string` is written. 344 + 345 + Params: 346 + builder = The `CodeBuilder` to use. 347 + param = The parameter to put. 348 + 349 + Returns: 350 + `builder` 351 + ++/ 352 CodeBuilder putExtended(T)(CodeBuilder builder, T param) 353 { 354 import std.range : isInputRange; 355 import std.conv : to; 356 import std.traits : isBuiltinType, isSomeFunction; 357 358 alias PType = T; 359 360 static if(is(PType : dstring) || isInputRange!PType) 361 builder.put(param); 362 else static if(is(PType : CodeFunc)) 363 param(builder); 364 else static if(is(PType == Variable)) 365 builder.put(param.name); 366 else static if(isBuiltinType!PType) 367 builder.put(param.to!dstring); 368 else static if(isSomeFunction!PType) // CodeFunc desctibes a delegate, so for functions we need to turn them into delegates first. 369 { 370 import std.functional : toDelegate; 371 372 auto del = param.toDelegate; 373 static assert(is(typeof(del) : CodeFunc), "Function of type '" ~ PType.stringof ~ "' is not convertable to a CodeFunc"); 374 375 builder.putExtended(del); 376 } 377 else 378 static assert(false, "Unsupported type: " ~ PType.stringof); 379 380 return builder; 381 } 382 /// 383 version(Jasterialise_Unittests) 384 unittest 385 { 386 auto builder = new CodeBuilder(); 387 388 builder.putExtended("Hello"d) // strings 389 .putExtended((CodeBuilder b) => b.put("World!")) // CodeFuncs 390 .putExtended(Variable("int", "myVar", null)) // Variables (only their names are written) 391 .putExtended(true); // Built-in D types (bools, ints, floats, etc.) 392 393 builder.data.should.equal("Hello\nWorld!\nmyVar\ntrue\n"d); 394 } 395 396 /++ 397 + Creates a function using the given data. 398 + 399 + Params: 400 + builder = The `CodeBuilder` to use. 401 + returnType = The name of the type that the function returns. 402 + name = The name of the function. 403 + params = The function's parameters. 404 + body_ = The `CodeFunc` which generates the code for the function's body. 405 + 406 + Returns: 407 + `builder` 408 + ++/ 409 CodeBuilder addFuncDeclaration(CodeBuilder builder, dstring returnType, dstring name, Variable[] params, CodeFunc body_) 410 { 411 import std.algorithm : map, joiner; 412 413 builder.put(returnType ~ " " ~ name, Yes.tabs, No.newLines); 414 415 builder.disable(); 416 builder.put("("); 417 builder.put(params.map!(v => v.typeName ~ " " ~ v.name) 418 .joiner(", ")); 419 builder.enable(); 420 builder.put(")", No.tabs); 421 422 builder.putScope(body_); 423 return builder; 424 } 425 /// 426 version(Jasterialise_Unittests) 427 unittest 428 { 429 auto builder = new CodeBuilder(); 430 431 builder.addFuncDeclaration("int", "sum", [Variable("int", "a"), Variable("int", "b")], (b){b.addReturn("a + b"d);}); 432 433 builder.data.should.equal("int sum(int a, int b)\n{\n\treturn a + b;\n}\n"); 434 } 435 436 /++ 437 + Creates a function using the given data. 438 + 439 + Params: 440 + returnType = The type that the function return. 441 + 442 + builder = The `CodeBuilder` to use. 443 + name = The name of the function. 444 + params = The function's parameters. 445 + body_ = The `CodeFunc` which generates the code for the function's body. 446 + 447 + Returns: 448 + `builder` 449 + ++/ 450 CodeBuilder addFuncDeclaration(returnType)(CodeBuilder builder, dstring name, Variable[] params, CodeFunc body_) 451 { 452 import std.traits : fullyQualifiedName; 453 454 return builder.addFuncDeclaration(fullyQualifiedName!returnType, name, params, body_); 455 } 456 /// 457 version(Jasterialise_Unittests) 458 unittest 459 { 460 auto builder = new CodeBuilder(); 461 462 builder.addFuncDeclaration!int("six", null, (b){b.addReturn("6"d);}); 463 464 builder.data.should.equal("int six()\n{\n\treturn 6;\n}\n"); 465 } 466 467 /++ 468 + Creates an import statement. 469 + 470 + Notes: 471 + If `selection` is `null`, then the entire module is imported. 472 + Otherwise, only the specified symbols are imported. 473 + 474 + Params: 475 + builder = The `CodeBuilder` to use. 476 + moduleName = The name of the module to import. 477 + selection = An array of which symbols to import from the module. 478 + 479 + Returns: 480 + `builder` 481 + ++/ 482 CodeBuilder addImport(CodeBuilder builder, dstring moduleName, dstring[] selection = null) 483 { 484 builder.put("import " ~ moduleName, Yes.tabs, No.newLines); 485 486 if(selection !is null) 487 { 488 import std.algorithm : joiner; 489 builder.disable(); // Disables both (tabbing and new lines) by default. 490 491 builder.put(" : "); 492 builder.put(selection.joiner(", "d)); 493 494 builder.enable(); // Likewise, enables both by default 495 } 496 497 builder.put(';', No.tabs); 498 return builder; 499 } 500 /// 501 version(Jasterialise_Unittests) 502 unittest 503 { 504 auto builder = new CodeBuilder(); 505 506 // Import entire module 507 builder.addImport("std.stdio"); 508 builder.data.should.equal("import std.stdio;\n"); 509 510 builder = new CodeBuilder(); // Just to keep the asserts clean to read. 511 512 513 // Selective imports 514 builder.addImport("std.stdio", ["readln"d, "writeln"d]); 515 builder.data.should.equal("import std.stdio : readln, writeln;\n"); 516 } 517 518 /++ 519 + Declares a variable, and returns a `Variable` which can be used to easily reference the variable. 520 + 521 + Notes: 522 + `valueFunc` may be `null`. 523 + 524 + Params: 525 + builder = The `CodeBuilder` to use. 526 + type = The name of the variable's type. 527 + name = The name of the variable. 528 + valueFunc = The `CodeFunc` which generates the code to set the variable's intial value. 529 + 530 + Returns: 531 + A `Variable` describing the variable declared by this function. 532 + ++/ 533 Variable addVariable(CodeBuilder builder, dstring type, dstring name, CodeFunc valueFunc = null) 534 { 535 builder.put(type ~ " " ~ name, Yes.tabs, No.newLines); 536 537 if(valueFunc !is null) 538 { 539 builder.disable(); // Disable automatic tabs and new lines. 540 541 builder.put(" = "); 542 valueFunc(builder); 543 544 builder.enable(); // Enable them both 545 } 546 547 builder.put(";", No.tabs); 548 return Variable(type, name, valueFunc); 549 } 550 /// 551 version(Jasterialise_Unittests) 552 unittest 553 { 554 auto builder = new CodeBuilder(); 555 556 // Declare the variable without setting it. 557 auto six = builder.addVariable("int", "six"); 558 builder.data.should.equal("int six;\n"d); 559 six.should.equal(Variable("int", "six")); 560 561 builder = new CodeBuilder(); 562 563 564 // Declare the variable, and set it's value. 565 CodeFunc func = (b){b.put("6");}; 566 six = builder.addVariable("int", "six", func); 567 568 builder.data.should.equal("int six = 6;\n"); 569 six.should.equal(Variable("int", "six", func)); 570 } 571 572 /// A helper function to more easily specify the variable's type. 573 Variable addVariable(T)(CodeBuilder builder, dstring name, CodeFunc valueFunc = null) 574 { 575 import std.traits : fullyQualifiedName; 576 577 return builder.addVariable(fullyQualifiedName!T, name, valueFunc); 578 } 579 /// 580 version(Jasterialise_Unittests) 581 unittest 582 { 583 auto builder = new CodeBuilder(); 584 585 builder.addVariable!int("six", (b){b.put("6");}); 586 builder.data.should.equal("int six = 6;\n"); 587 } 588 589 /// A helper function for `addVariable` which creates an alias. 590 Variable addAlias(CodeBuilder builder, dstring name, CodeFunc valueFunc) 591 { 592 return builder.addVariable("alias", name, valueFunc); 593 } 594 /// 595 version(Jasterialise_Unittests) 596 unittest 597 { 598 auto builder = new CodeBuilder(); 599 builder.addAlias("SomeType", (b){b.put("int");}); 600 builder.data.should.equal("alias SomeType = int;\n"); 601 } 602 603 /// A helper function for `addVariable` which creates an enum value. 604 Variable addEnumValue(CodeBuilder builder, dstring name, CodeFunc valueFunc) 605 { 606 return builder.addVariable("enum", name, valueFunc); 607 } 608 /// 609 version(Jasterialise_Unittests) 610 unittest 611 { 612 auto builder = new CodeBuilder(); 613 builder.addEnumValue("SomeValue", (b){b.put("6");}); 614 builder.data.should.equal("enum SomeValue = 6;\n"); 615 } 616 617 /++ 618 + Creates a return statement. 619 + 620 + Notes: 621 + `T` can be any type supported by `putExtended`. 622 + 623 + Params: 624 + builder = The `CodeBuilder` to use. 625 + code = The code to use in the return statement. 626 + 627 + Returns: 628 + `builder` 629 + ++/ 630 CodeBuilder addReturn(T)(CodeBuilder builder, T code) 631 { 632 builder.put("return ", Yes.tabs, No.newLines); 633 builder.disable(); 634 635 builder.putExtended(code); 636 637 builder.enable(); 638 builder.put(";", No.tabs, Yes.newLines); 639 640 return builder; 641 } 642 /// 643 version(Jasterialise_Unittests) 644 unittest 645 { 646 auto builder = new CodeBuilder(); 647 648 // Option #1: Pass in a dstring, and it'll be added as-is. 649 builder.addReturn("21 * 8"d); 650 builder.data.should.equal("return 21 * 8;\n"); 651 652 builder = new CodeBuilder(); 653 654 655 // Option #2: Pass in an instance of Variable, and the variable's name is added. 656 builder.addReturn(Variable("int", "someNumber")); 657 builder.data.should.equal("return someNumber;\n"); 658 659 builder = new CodeBuilder(); 660 661 662 // Option #3: Pass in a CodeFunc, and let it deal with generating the code it needs. 663 CodeFunc func = (b){b.put("200 / someNumber");}; 664 builder.addReturn(func); 665 builder.data.should.equal("return 200 / someNumber;\n"); 666 } 667 668 /++ 669 + Creates a call to a function. 670 + 671 + Notes: 672 + `params` can be made up of any combination of values supported by `putExtended`. 673 + 674 + Strings $(B won't) be automatically enclosed between speech marks('"'). 675 + 676 + Params: 677 + semicolon = If `Yes.semicolon`, then a ';' is inserted at the end of the function call. 678 + 679 + builder = The `CodeBuilder` to use. 680 + funcName = The name of the function to call. 681 + params = The parameters to pass to the function. 682 + 683 + Returns: 684 + `builder` 685 + ++/ 686 CodeBuilder addFuncCall(Flag!"semicolon" semicolon = Yes.semicolon, Params...)(CodeBuilder builder, dstring funcName, Params params) 687 { 688 import std.conv : to; 689 import std.range : isInputRange; 690 import std.traits : isBuiltinType; 691 692 builder.put(funcName, Yes.tabs, No.newLines); 693 builder.disable(); 694 builder.put('('); 695 696 foreach(i, param; params) 697 { 698 builder.putExtended(param); 699 700 static if(i != params.length - 1) 701 builder.put(", "); 702 } 703 704 builder.enable(); 705 builder.put(')', No.tabs, No.newLines); 706 707 static if(semicolon) 708 builder.put(';', No.tabs); 709 710 return builder; 711 } 712 /// 713 version(Jasterialise_Unittests) 714 unittest 715 { 716 auto builder = new CodeBuilder(); 717 718 // DStrings(Including input ranges of them), CodeFuncs, built-in types(int, bool, float, etc.), and Variables can all be passed as parameters. 719 dstring str = "\"Hello\""d; 720 CodeFunc func = (b){b.putString("World!");}; 721 Variable vari = Variable("int", "someVar"); 722 723 builder.addFuncCall("writeln", str, func, vari); 724 725 builder.data.should.equal("writeln(\"Hello\", \"World!\", someVar);\n"); 726 }