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 }