1 module workspaced.api; 2 3 import core.time; 4 import dparse.lexer; 5 import painlessjson; 6 import standardpaths; 7 import std.algorithm; 8 import std.conv; 9 import std.exception; 10 import std.file; 11 import std.json; 12 import std.meta; 13 import std.path; 14 import std.regex; 15 import std.traits; 16 import std.typecons; 17 18 /// 19 alias ImportPathProvider = string[]delegate(); 20 /// 21 alias BroadcastCallback = void delegate(WorkspaceD, WorkspaceD.Instance, JSONValue); 22 /// 23 alias ComponentBindFailCallback = void delegate(WorkspaceD.Instance, ComponentFactory); 24 25 /// Will never call this function 26 enum ignoredFunc; 27 28 /// Component call 29 struct ComponentInfo 30 { 31 /// Name of the component 32 string name; 33 } 34 35 ComponentInfo component(string name) 36 { 37 return ComponentInfo(name); 38 } 39 40 mixin template DefaultComponentWrapper() 41 { 42 @ignoredFunc 43 { 44 WorkspaceD workspaced; 45 WorkspaceD.Instance refInstance; 46 47 WorkspaceD.Instance instance() const @property 48 { 49 if (refInstance) 50 return cast() refInstance; 51 else 52 throw new Exception("Attempted to access instance in a global context"); 53 } 54 55 WorkspaceD.Instance instance(WorkspaceD.Instance instance) @property 56 { 57 return refInstance = instance; 58 } 59 60 string[] importPaths() const @property 61 { 62 return instance.importPathProvider ? instance.importPathProvider() : []; 63 } 64 65 string[] stringImportPaths() const @property 66 { 67 return instance.stringImportPathProvider ? instance.stringImportPathProvider() : []; 68 } 69 70 string[] importFiles() const @property 71 { 72 return instance.importFilesProvider ? instance.importFilesProvider() : []; 73 } 74 75 ref ImportPathProvider importPathProvider() @property 76 { 77 return instance.importPathProvider; 78 } 79 80 ref ImportPathProvider stringImportPathProvider() @property 81 { 82 return instance.stringImportPathProvider; 83 } 84 85 ref ImportPathProvider importFilesProvider() @property 86 { 87 return instance.importFilesProvider; 88 } 89 90 ref Configuration config() @property 91 { 92 if (refInstance) 93 return refInstance.config; 94 else if (workspaced) 95 return workspaced.globalConfiguration; 96 else 97 assert(false, "Unbound component trying to access config."); 98 } 99 100 T get(T)() 101 { 102 if (refInstance) 103 return refInstance.get!T; 104 else if (workspaced) 105 return workspaced.get!T; 106 else 107 assert(false, "Unbound component trying to get component " ~ T.stringof ~ "."); 108 } 109 110 string cwd() @property const 111 { 112 return instance.cwd; 113 } 114 115 override void shutdown() 116 { 117 } 118 119 override void bind(WorkspaceD workspaced, WorkspaceD.Instance instance) 120 { 121 this.workspaced = workspaced; 122 this.instance = instance; 123 static if (__traits(hasMember, typeof(this).init, "load")) 124 load(); 125 } 126 127 import std.conv; 128 import std.json : JSONValue; 129 import std.traits : isFunction, hasUDA, ParameterDefaults, Parameters, 130 ReturnType; 131 import painlessjson; 132 133 override Future!JSONValue run(string method, JSONValue[] args) 134 { 135 static foreach (member; __traits(derivedMembers, typeof(this))) 136 static if (member[0] != '_' && __traits(compiles, __traits(getMember, 137 typeof(this).init, member)) && __traits(getProtection, __traits(getMember, typeof(this).init, 138 member)) == "public" && __traits(compiles, isFunction!(__traits(getMember, 139 typeof(this).init, member))) && isFunction!(__traits(getMember, 140 typeof(this).init, member)) && !hasUDA!(__traits(getMember, typeof(this).init, 141 member), ignoredFunc) && !__traits(isTemplate, __traits(getMember, 142 typeof(this).init, member))) 143 if (method == member) 144 return runMethod!member(args); 145 throw new Exception("Method " ~ method ~ " not found."); 146 } 147 148 Future!JSONValue runMethod(string method)(JSONValue[] args) 149 { 150 int matches; 151 static foreach (overload; __traits(getOverloads, typeof(this), method)) 152 { 153 if (matchesOverload!overload(args)) 154 matches++; 155 } 156 if (matches == 0) 157 throw new Exception("No suitable overload found for " ~ method ~ "."); 158 if (matches > 1) 159 throw new Exception("Multiple overloads found for " ~ method ~ "."); 160 static foreach (overload; __traits(getOverloads, typeof(this), method)) 161 { 162 if (matchesOverload!overload(args)) 163 return runOverload!overload(args); 164 } 165 assert(false); 166 } 167 168 Future!JSONValue runOverload(alias fun)(JSONValue[] args) 169 { 170 mixin(generateOverloadCall!fun); 171 } 172 173 static string generateOverloadCall(alias fun)() 174 { 175 string call = "fun("; 176 static foreach (i, T; Parameters!fun) 177 call ~= "args[" ~ i.to!string ~ "].fromJSON!(" ~ T.stringof ~ "), "; 178 call ~= ")"; 179 static if (is(ReturnType!fun : Future!T, T)) 180 { 181 static if (is(T == void)) 182 string conv = "ret.finish(JSONValue(null));"; 183 else 184 string conv = "ret.finish(v.value.toJSON);"; 185 return "auto ret = new Future!JSONValue; auto v = " ~ call 186 ~ "; v.onDone = { if (v.exception) ret.error(v.exception); else " 187 ~ conv ~ " }; return ret;"; 188 } 189 else static if (is(ReturnType!fun == void)) 190 return call ~ "; return Future!JSONValue.fromResult(JSONValue(null));"; 191 else 192 return "return Future!JSONValue.fromResult(" ~ call ~ ".toJSON);"; 193 } 194 } 195 } 196 197 bool matchesOverload(alias fun)(JSONValue[] args) 198 { 199 if (args.length > Parameters!fun.length) 200 return false; 201 static foreach (i, def; ParameterDefaults!fun) 202 { 203 static if (is(def == void)) 204 { 205 if (i >= args.length) 206 return false; 207 else if (!checkType!(Parameters!fun[i])(args[i])) 208 return false; 209 } 210 } 211 return true; 212 } 213 214 bool checkType(T)(JSONValue value) 215 { 216 final switch (value.type) 217 { 218 case JSON_TYPE.ARRAY: 219 static if (isStaticArray!T) 220 return T.length == value.array.length 221 && value.array.all!(checkType!(typeof(T.init[0]))); 222 else static if (isDynamicArray!T) return value.array.all!(checkType!(typeof(T.init[0]))); 223 else static if (is(T : Tuple!Args, Args...)) 224 { 225 if (value.array.length != Args.length) 226 return false; 227 static foreach (i, Arg; Args) 228 if (!checkType!Arg(value.array[i])) 229 return false; 230 return true; 231 } 232 else 233 return false; 234 case JSON_TYPE.FALSE: 235 case JSON_TYPE.TRUE: 236 return is(T : bool); 237 case JSON_TYPE.FLOAT: 238 return isNumeric!T; 239 case JSON_TYPE.INTEGER: 240 case JSON_TYPE.UINTEGER: 241 return isIntegral!T; 242 case JSON_TYPE.NULL: 243 static if (is(T == class) || isArray!T || isPointer!T || is(T : Nullable!U, U)) 244 return true; 245 else 246 return false; 247 case JSON_TYPE.OBJECT: 248 return is(T == class) || is(T == struct); 249 case JSON_TYPE.STRING: 250 return isSomeString!T; 251 } 252 } 253 254 interface ComponentWrapper 255 { 256 void bind(WorkspaceD workspaced, WorkspaceD.Instance instance); 257 Future!JSONValue run(string method, JSONValue[] args); 258 void shutdown(); 259 } 260 261 interface ComponentFactory 262 { 263 ComponentWrapper create(WorkspaceD workspaced, WorkspaceD.Instance instance); 264 ComponentInfo info() @property; 265 } 266 267 struct ComponentFactoryInstance 268 { 269 ComponentFactory factory; 270 bool autoRegister; 271 alias factory this; 272 } 273 274 struct ComponentWrapperInstance 275 { 276 ComponentWrapper wrapper; 277 ComponentInfo info; 278 } 279 280 struct Configuration 281 { 282 /// JSON containing base configuration formatted as {[component]:{key:value pairs}} 283 JSONValue base; 284 285 bool get(string component, string key, out JSONValue val) 286 { 287 if (base.type != JSON_TYPE.OBJECT) 288 { 289 JSONValue[string] tmp; 290 base = JSONValue(tmp); 291 } 292 auto com = component in base.object; 293 if (!com) 294 return false; 295 auto v = key in *com; 296 if (!v) 297 return false; 298 val = *v; 299 return true; 300 } 301 302 T get(T)(string component, string key, T defaultValue = T.init) 303 { 304 JSONValue ret; 305 if (!get(component, key, ret)) 306 return defaultValue; 307 return ret.fromJSON!T; 308 } 309 310 bool set(T)(string component, string key, T value) 311 { 312 if (base.type != JSON_TYPE.OBJECT) 313 { 314 JSONValue[string] tmp; 315 base = JSONValue(tmp); 316 } 317 auto com = component in base.object; 318 if (!com) 319 { 320 JSONValue[string] val; 321 val[key] = value.toJSON; 322 base.object[component] = JSONValue(val); 323 } 324 else 325 { 326 com.object[key] = value.toJSON; 327 } 328 return true; 329 } 330 331 /// Same as init but might make nicer code. 332 static immutable Configuration none = Configuration.init; 333 334 /// Loads unset keys from global, keeps existing keys 335 void loadBase(Configuration global) 336 { 337 if (global.base.type != JSON_TYPE.OBJECT) 338 return; 339 340 if (base.type != JSON_TYPE.OBJECT) 341 base = global.base.dupJson; 342 else 343 { 344 foreach (component, config; global.base.object) 345 { 346 auto existing = component in base.object; 347 if (!existing || config.type != JSON_TYPE.OBJECT) 348 base.object[component] = config.dupJson; 349 else 350 { 351 foreach (key, value; config.object) 352 { 353 auto existingValue = key in *existing; 354 if (!existingValue) 355 (*existing)[key] = value.dupJson; 356 } 357 } 358 } 359 } 360 } 361 } 362 363 private JSONValue dupJson(JSONValue v) 364 { 365 switch (v.type) 366 { 367 case JSON_TYPE.OBJECT: 368 return JSONValue(v.object.dup); 369 case JSON_TYPE.ARRAY: 370 return JSONValue(v.array.dup); 371 default: 372 return v; 373 } 374 } 375 376 /// WorkspaceD instance holding plugins. 377 class WorkspaceD 378 { 379 static class Instance 380 { 381 string cwd; 382 ComponentWrapperInstance[] instanceComponents; 383 Configuration config; 384 385 string[] importPaths() const @property 386 { 387 return importPathProvider ? importPathProvider() : []; 388 } 389 390 string[] stringImportPaths() const @property 391 { 392 return stringImportPathProvider ? stringImportPathProvider() : []; 393 } 394 395 string[] importFiles() const @property 396 { 397 return importFilesProvider ? importFilesProvider() : []; 398 } 399 400 void shutdown() 401 { 402 foreach (ref com; instanceComponents) 403 com.wrapper.shutdown(); 404 instanceComponents = null; 405 } 406 407 ImportPathProvider importPathProvider; 408 ImportPathProvider stringImportPathProvider; 409 ImportPathProvider importFilesProvider; 410 411 Future!JSONValue run(WorkspaceD workspaced, string component, string method, JSONValue[] args) 412 { 413 foreach (ref com; instanceComponents) 414 if (com.info.name == component) 415 return com.wrapper.run(method, args); 416 throw new Exception("Component '" ~ component ~ "' not found"); 417 } 418 419 T get(T)() 420 { 421 auto name = getUDAs!(T, ComponentInfo)[0].name; 422 foreach (com; instanceComponents) 423 if (com.info.name == name) 424 return cast(T) com.wrapper; 425 throw new Exception( 426 "Attempted to get unknown instance component " ~ T.stringof ~ " in instance cwd:" ~ cwd); 427 } 428 429 bool has(T)() 430 { 431 auto name = getUDAs!(T, ComponentInfo)[0].name; 432 foreach (com; instanceComponents) 433 if (com.info.name == name) 434 return true; 435 return false; 436 } 437 438 /// Loads a registered component which didn't have auto register on just for this instance. 439 /// Returns: false instead of using the onBindFail callback on failure. 440 /// Throws: Exception if component was not regsitered in workspaced. 441 bool attach(T)(WorkspaceD workspaced) 442 { 443 string name = getUDAs!(T, ComponentInfo)[0].name; 444 foreach (factory; workspaced.components) 445 { 446 if (factory.info.name == name) 447 { 448 auto inst = factory.create(workspaced, this); 449 if (inst) 450 { 451 instanceComponents ~= ComponentWrapperInstance(inst, info); 452 return true; 453 } 454 else 455 return false; 456 } 457 } 458 throw new Exception("Component not found"); 459 } 460 } 461 462 BroadcastCallback onBroadcast; 463 ComponentBindFailCallback onBindFail; 464 465 Instance[] instances; 466 /// Base global configuration for new instances, does not modify existing ones. 467 Configuration globalConfiguration; 468 ComponentWrapperInstance[] globalComponents; 469 ComponentFactoryInstance[] components; 470 StringCache stringCache; 471 472 this() 473 { 474 stringCache = StringCache(StringCache.defaultBucketCount * 4); 475 } 476 477 void shutdown() 478 { 479 foreach (ref instance; instances) 480 instance.shutdown(); 481 instances = null; 482 foreach (ref com; globalComponents) 483 com.wrapper.shutdown(); 484 globalComponents = null; 485 components = null; 486 } 487 488 void broadcast(WorkspaceD.Instance instance, JSONValue value) 489 { 490 if (onBroadcast) 491 onBroadcast(this, instance, value); 492 } 493 494 Instance getInstance(string cwd) nothrow 495 { 496 cwd = buildNormalizedPath(cwd); 497 foreach (instance; instances) 498 if (instance.cwd == cwd) 499 return instance; 500 return null; 501 } 502 503 T get(T)() 504 { 505 auto name = getUDAs!(T, ComponentInfo)[0].name; 506 foreach (com; globalComponents) 507 if (com.info.name == name) 508 return cast(T) com.wrapper; 509 throw new Exception("Attempted to get unknown global component " ~ T.stringof); 510 } 511 512 bool has(T)() 513 { 514 auto name = getUDAs!(T, ComponentInfo)[0].name; 515 foreach (com; globalComponents) 516 if (com.info.name == name) 517 return true; 518 return false; 519 } 520 521 T get(T)(string cwd) 522 { 523 if (!cwd.length) 524 return this.get!T; 525 auto inst = getInstance(cwd); 526 if (inst is null) 527 throw new Exception("cwd '" ~ cwd ~ "' not found"); 528 return inst.get!T; 529 } 530 531 bool has(T)(string cwd) 532 { 533 auto inst = getInstance(cwd); 534 if (inst is null) 535 return false; 536 return inst.has!T; 537 } 538 539 Future!JSONValue run(string cwd, string component, string method, JSONValue[] args) 540 { 541 auto instance = getInstance(cwd); 542 if (instance is null) 543 throw new Exception("cwd '" ~ cwd ~ "' not found"); 544 return instance.run(this, component, method, args); 545 } 546 547 Future!JSONValue run(string component, string method, JSONValue[] args) 548 { 549 foreach (ref com; globalComponents) 550 if (com.info.name == component) 551 return com.wrapper.run(method, args); 552 throw new Exception("Global component '" ~ component ~ "' not found"); 553 } 554 555 ComponentFactory register(T)(bool autoRegister = true) 556 { 557 ComponentFactory factory; 558 static foreach (attr; __traits(getAttributes, T)) 559 static if (is(attr == class) && is(attr : ComponentFactory)) 560 factory = new attr; 561 if (factory is null) 562 factory = new DefaultComponentFactory!T; 563 components ~= ComponentFactoryInstance(factory, autoRegister); 564 auto info = factory.info; 565 auto glob = factory.create(this, null); 566 if (glob) 567 globalComponents ~= ComponentWrapperInstance(glob, info); 568 if (autoRegister) 569 foreach (ref instance; instances) 570 { 571 auto inst = factory.create(this, instance); 572 if (inst) 573 instance.instanceComponents ~= ComponentWrapperInstance(inst, info); 574 else if (onBindFail) 575 onBindFail(instance, factory); 576 } 577 static if (__traits(compiles, T.registered(this))) 578 T.registered(this); 579 else static if (__traits(compiles, T.registered())) 580 T.registered(); 581 return factory; 582 } 583 584 /// Creates a new workspace with the given cwd with optional config overrides and preload components for non-autoRegister components. 585 /// Throws: Exception if normalized cwd already exists as instance. 586 Instance addInstance(string cwd, 587 Configuration configOverrides = Configuration.none, string[] preloadComponents = []) 588 { 589 cwd = buildNormalizedPath(cwd); 590 if (instances.canFind!(a => a.cwd == cwd)) 591 throw new Exception("Instance with cwd '" ~ cwd ~ "' already exists!"); 592 auto inst = new Instance(); 593 inst.cwd = cwd; 594 configOverrides.loadBase(globalConfiguration); 595 inst.config = configOverrides; 596 instances ~= inst; 597 foreach (name; preloadComponents) 598 { 599 foreach (factory; components) 600 { 601 if (!factory.autoRegister && factory.info.name == name) 602 { 603 auto wrap = factory.create(this, inst); 604 if (wrap) 605 inst.instanceComponents ~= ComponentWrapperInstance(wrap, factory.info); 606 else if (onBindFail) 607 onBindFail(inst, factory); 608 break; 609 } 610 } 611 } 612 foreach (factory; components) 613 { 614 if (factory.autoRegister) 615 { 616 auto wrap = factory.create(this, inst); 617 if (wrap) 618 inst.instanceComponents ~= ComponentWrapperInstance(wrap, factory.info); 619 else if (onBindFail) 620 onBindFail(inst, factory); 621 } 622 } 623 return inst; 624 } 625 626 bool removeInstance(string cwd) 627 { 628 cwd = buildNormalizedPath(cwd); 629 foreach (i, instance; instances) 630 if (instance.cwd == cwd) 631 { 632 foreach (com; instance.instanceComponents) 633 destroy(com.wrapper); 634 destroy(instance); 635 instances = instances.remove(i); 636 return true; 637 } 638 return false; 639 } 640 641 bool attach(Instance instance, string component) 642 { 643 foreach (factory; components) 644 { 645 if (factory.info.name == component) 646 { 647 auto wrap = factory.create(this, instance); 648 if (wrap) 649 { 650 instance.instanceComponents ~= ComponentWrapperInstance(wrap, factory.info); 651 return true; 652 } 653 else 654 return false; 655 } 656 } 657 return false; 658 } 659 } 660 661 class DefaultComponentFactory(T : ComponentWrapper) : ComponentFactory 662 { 663 ComponentWrapper create(WorkspaceD workspaced, WorkspaceD.Instance instance) 664 { 665 auto wrapper = new T(); 666 try 667 { 668 wrapper.bind(workspaced, instance); 669 return wrapper; 670 } 671 catch (Exception e) 672 { 673 return null; 674 } 675 } 676 677 ComponentInfo info() @property 678 { 679 alias udas = getUDAs!(T, ComponentInfo); 680 static assert(udas.length == 1, "Can't construct default component factory for " 681 ~ T.stringof ~ ", expected exactly 1 ComponentInfo instance attached to the type"); 682 return udas[0]; 683 } 684 } 685 686 /// Describes what to insert/replace/delete to do something 687 struct CodeReplacement 688 { 689 /// Range what to replace. If both indices are the same its inserting. 690 size_t[2] range; 691 /// Content to replace it with. Empty means remove. 692 string content; 693 694 /// Applies this edit to a string. 695 string apply(string code) 696 { 697 size_t min = range[0]; 698 size_t max = range[1]; 699 if (min > max) 700 { 701 min = range[1]; 702 max = range[0]; 703 } 704 if (min >= code.length) 705 return code ~ content; 706 if (max >= code.length) 707 return code[0 .. min] ~ content; 708 return code[0 .. min] ~ content ~ code[max .. $]; 709 } 710 } 711 712 /// Code replacements mapped to a file 713 struct FileChanges 714 { 715 /// File path to change. 716 string file; 717 /// Replacements to apply. 718 CodeReplacement[] replacements; 719 } 720 721 package bool getConfigPath(string file, ref string retPath) 722 { 723 foreach (dir; standardPaths(StandardPath.config, "workspace-d")) 724 { 725 auto path = buildPath(dir, file); 726 if (path.exists) 727 { 728 retPath = path; 729 return true; 730 } 731 } 732 return false; 733 } 734 735 enum verRegex = ctRegex!`(\d+)\.(\d+)\.(\d+)`; 736 bool checkVersion(string ver, int[3] target) 737 { 738 auto match = ver.matchFirst(verRegex); 739 if (!match) 740 return false; 741 int major = match[1].to!int; 742 int minor = match[2].to!int; 743 int patch = match[3].to!int; 744 if (major > target[0]) 745 return true; 746 if (major == target[0] && minor > target[1]) 747 return true; 748 if (major == target[0] && minor == target[1] && patch >= target[2]) 749 return true; 750 return false; 751 } 752 753 package string getVersionAndFixPath(ref string execPath) 754 { 755 import std.process; 756 757 try 758 { 759 return execute([execPath, "--version"]).output; 760 } 761 catch (ProcessException e) 762 { 763 auto newPath = buildPath(thisExePath.dirName, execPath.baseName); 764 if (exists(newPath)) 765 { 766 execPath = newPath; 767 return execute([execPath, "--version"]).output; 768 } 769 throw e; 770 } 771 } 772 773 class Future(T) 774 { 775 static if (!is(T == void)) 776 T value; 777 Throwable exception; 778 bool has; 779 void delegate() _onDone; 780 781 /// Sets the onDone callback if no value has been set yet or calls immediately if the value has already been set or was set during setting the callback. 782 /// Crashes with an assert error if attempting to override an existing callback (i.e. calling this function on the same object twice). 783 void onDone(void delegate() callback) @property 784 { 785 assert(!_onDone); 786 if (has) 787 callback(); 788 else 789 { 790 bool called; 791 _onDone = { called = true; callback(); }; 792 if (has && !called) 793 callback(); 794 } 795 } 796 797 static if (is(T == void)) 798 static Future!void finished() 799 { 800 auto ret = new Future!void; 801 ret.has = true; 802 return ret; 803 } 804 else 805 static Future!T fromResult(T value) 806 { 807 auto ret = new Future!T; 808 ret.value = value; 809 ret.has = true; 810 return ret; 811 } 812 813 static Future!T async(T delegate() cb) 814 { 815 import core.thread : Thread; 816 817 auto ret = new Future!T; 818 new Thread({ 819 try 820 { 821 static if (is(T == void)) 822 { 823 cb(); 824 ret.finish(); 825 } 826 else 827 ret.finish(cb()); 828 } 829 catch (Throwable t) 830 { 831 ret.error(t); 832 } 833 }).start(); 834 return ret; 835 } 836 837 static Future!T fromError(T)(Throwable error) 838 { 839 auto ret = new Future!T; 840 ret.error = error; 841 ret.has = true; 842 return ret; 843 } 844 845 static if (is(T == void)) 846 void finish() 847 { 848 assert(!has); 849 has = true; 850 if (_onDone) 851 _onDone(); 852 } 853 else 854 void finish(T value) 855 { 856 assert(!has); 857 this.value = value; 858 has = true; 859 if (_onDone) 860 _onDone(); 861 } 862 863 void error(Throwable t) 864 { 865 assert(!has); 866 exception = t; 867 has = true; 868 if (_onDone) 869 _onDone(); 870 } 871 872 /// Waits for the result of this future using Thread.sleep 873 T getBlocking(alias sleepDur = 1.msecs)() 874 { 875 import core.thread : Thread; 876 877 while (!has) 878 Thread.sleep(sleepDur); 879 if (exception) 880 throw exception; 881 static if (!is(T == void)) 882 return value; 883 } 884 885 /// Waits for the result of this future using Fiber.yield 886 T getYield() 887 { 888 import core.thread : Fiber; 889 890 while (!has) 891 Fiber.yield(); 892 if (exception) 893 throw exception; 894 static if (!is(T == void)) 895 return value; 896 } 897 } 898 899 version (unittest) 900 { 901 struct TestingWorkspace 902 { 903 string directory; 904 905 @disable this(this); 906 907 this(string path) 908 { 909 if (path.exists) 910 throw new Exception("Path already exists"); 911 directory = path; 912 mkdir(path); 913 } 914 915 ~this() 916 { 917 rmdirRecurse(directory); 918 } 919 920 string getPath(string path) 921 { 922 return buildPath(directory, path); 923 } 924 925 void createDir(string dir) 926 { 927 mkdirRecurse(getPath(dir)); 928 } 929 930 void writeFile(string path, string content) 931 { 932 write(getPath(path), content); 933 } 934 } 935 936 TestingWorkspace makeTemporaryTestingWorkspace() 937 { 938 import std.random; 939 940 return TestingWorkspace(buildPath(tempDir, "workspace-d-test-" ~ uniform(0, 941 int.max).to!string(36))); 942 } 943 }