1 module workspaced.backend;
2 
3 import dparse.lexer : StringCache;
4 
5 import std.algorithm : canFind, map, max, min, remove, startsWith;
6 import std.array : array;
7 import std.conv;
8 import std.file : exists, mkdir, mkdirRecurse, rmdirRecurse, tempDir, write;
9 import std.parallelism : defaultPoolThreads, TaskPool;
10 import std.path : buildNormalizedPath, buildPath;
11 import std.range : chain;
12 import std.sumtype : match, SumType, This;
13 import std.traits : getUDAs, isSomeString;
14 
15 import workspaced.api;
16 
17 struct Configuration
18 {
19 	alias ValueT = SumType!(typeof(null), string, bool, long, double, This[], This[string]);
20 
21 	private static ValueT toValueT(T)(T value)
22 	{
23 		static if (is(immutable T == immutable ValueT))
24 			return value;
25 		else static if (is(T : U[], U))
26 		{
27 			ValueT[] ret = new ValueT[value.length];
28 			foreach (i, v; value)
29 				ret[i] = toValueT(v);
30 			return ValueT(ret);
31 		}
32 		else static if (is(T : U[string], U))
33 		{
34 			ValueT[string] ret;
35 			foreach (k, v; value)
36 				ret[k] = toValueT(v);
37 			return ValueT(ret);
38 		}
39 		else
40 		{
41 			return ValueT(value);
42 		}
43 	}
44 
45 	static T toType(T)(ValueT value)
46 	{
47 		return value.match!(
48 			(typeof(null) n) {
49 				if (false) return T.init; // make return type T
50 
51 				static if (__traits(compiles, T.init is null) && T.init is null)
52 					return T.init;
53 				else
54 					throw new Exception("Cannot convert null to type " ~ T.stringof);
55 			},
56 			(ValueT[] v) {
57 				if (false) return T.init; // make return type T
58 
59 				static if (is(typeof(T.init[0])))
60 				{
61 					T ret;
62 					ret.reserve(v.length);
63 					foreach (i; v)
64 						ret ~= toType!(typeof(T.init[0]))(i);
65 					return ret;
66 				}
67 				else
68 					throw new Exception("Cannot convert array to type " ~ T.stringof);
69 			},
70 			(ValueT[string] m) {
71 				if (false) return T.init; // make return type T
72 
73 				static if (is(typeof(T.init[""])))
74 				{
75 					T ret;
76 					foreach (k, v; m)
77 						ret[k] = toType!(typeof(ret[k]))(v);
78 					return ret;
79 				}
80 				else
81 					throw new Exception("Cannot convert map to type " ~ T.stringof);
82 			},
83 			(s) {
84 				if (false) return T.init; // make return type T
85 
86 				static if (is(T : typeof(s)))
87 					return cast(T) s;
88 				else static if (__traits(compiles, s.to!T))
89 					return s.to!T;
90 				else
91 					throw new Exception("Cannot convert " ~ typeof(s).stringof
92 						~ " to type " ~ T.stringof);
93 			}
94 		);
95 	}
96 
97 	static struct Section
98 	{
99 		ValueT[string] values;
100 	}
101 
102 	/// base configuration formatted as {[component]:{key:value pairs}}
103 	Section[string] base;
104 
105 	bool get(string component, string key, out ValueT val) const
106 	{
107 		auto com = component in base;
108 		if (!com)
109 			return false;
110 		auto v = key in com.values;
111 		if (!v)
112 			return false;
113 		val = *v;
114 		return true;
115 	}
116 
117 	T get(T)(string component, string key, T defaultValue = T.init) inout
118 	{
119 		ValueT ret;
120 		if (!get(component, key, ret))
121 			return defaultValue;
122 		return toType!T(ret);
123 	}
124 
125 	bool set(T)(string component, string key, T value)
126 	{
127 		if (auto com = component in base)
128 		{
129 			com.values[key] = toValueT(value);
130 		}
131 		else
132 		{
133 			ValueT[string] val;
134 			val[key] = toValueT(value);
135 			base[component] = Section(val);
136 		}
137 		return true;
138 	}
139 
140 	/// Same as init but might make nicer code.
141 	enum none = Configuration.init;
142 
143 	/// Loads unset keys from global, keeps existing keys
144 	void loadBase(Configuration global)
145 	{
146 		if (base is null)
147 			base = global.base.dup;
148 		else
149 		{
150 			foreach (component, config; global.base)
151 			{
152 				auto existing = component in base;
153 				if (!existing)
154 					base[component] = config.deepCopy;
155 				else
156 				{
157 					foreach (key, value; config.values)
158 					{
159 						auto existingValue = key in existing.values;
160 						if (!existingValue)
161 							existing.values[key] = value.deepCopy;
162 					}
163 				}
164 			}
165 		}
166 	}
167 }
168 
169 private T deepCopy(T)(T value)
170 {
171 	static if (is(T : typeof(null)) || __traits(isPOD, T))
172 		return value;
173 	else static if (is(T == Configuration.ValueT))
174 	{
175 		return value.match!(v => deepCopy(v));
176 	}
177 	else static if (is(T : Configuration.Section))
178 	{
179 		return Configuration.Section(deepCopy(value.values));
180 	}
181 	else static if (is(T : Configuration.ValueT[]))
182 	{
183 		auto copy = new Configuration.ValueT[value.length];
184 		foreach (i, ref v; copy)
185 			v = deepCopy(value[i]);
186 		return copy;
187 	}
188 	else static if (is(T : Configuration.ValueT[string]))
189 	{
190 		Configuration.ValueT[string] copy;
191 		foreach (k, v; value)
192 			copy[k] = deepCopy(v);
193 		return copy;
194 	}
195 	else
196 		return value.dup;
197 }
198 
199 /// WorkspaceD instance holding plugins.
200 class WorkspaceD
201 {
202 	static class Instance
203 	{
204 		string cwd;
205 		ComponentWrapperInstance[] instanceComponents;
206 		Configuration config;
207 
208 		string[] importPaths() const @property nothrow
209 		{
210 			return importPathProvider ? importPathProvider() : [];
211 		}
212 
213 		string[] stringImportPaths() const @property nothrow
214 		{
215 			return stringImportPathProvider ? stringImportPathProvider() : [];
216 		}
217 
218 		string[] importFiles() const @property nothrow
219 		{
220 			return importFilesProvider ? importFilesProvider() : [];
221 		}
222 
223 		void shutdown(bool dtor = false)
224 		{
225 			foreach (ref com; instanceComponents)
226 				com.wrapper.shutdown(dtor);
227 			instanceComponents = null;
228 		}
229 
230 		ImportPathProvider importPathProvider;
231 		ImportPathProvider stringImportPathProvider;
232 		ImportPathProvider importFilesProvider;
233 		IdentifierListProvider projectVersionsProvider;
234 		IdentifierListProvider debugSpecificationsProvider;
235 
236 		/* virtual */
237 		void onBeforeAccessComponent(ComponentInfo) const
238 		{
239 		}
240 
241 		/* virtual */
242 		bool checkHasComponent(ComponentInfo info) const nothrow
243 		{
244 			foreach (com; instanceComponents)
245 				if (com.info.name == info.name)
246 					return true;
247 			return false;
248 		}
249 
250 		inout(T) get(T)() inout
251 		{
252 			auto info = getUDAs!(T, ComponentInfoParams)[0];
253 			onBeforeAccessComponent(ComponentInfo(info, typeid(T)));
254 			foreach (com; instanceComponents)
255 				if (com.info.name == info.name)
256 					return cast(inout T) com.wrapper;
257 			throw new Exception(
258 					"Attempted to get unknown instance component " ~ T.stringof
259 					~ " in instance cwd:" ~ cwd);
260 		}
261 
262 		bool has(T)() const nothrow
263 		{
264 			auto info = getUDAs!(T, ComponentInfoParams)[0];
265 			return checkHasComponent(ComponentInfo(info, typeid(T)));
266 		}
267 
268 		/// Shuts down an attached component and removes it from this component
269 		/// list. If you plan to remove all components, call $(LREF shutdown)
270 		/// instead.
271 		/// Returns: `true` if the component was loaded and is now unloaded and
272 		///          removed or `false` if the component wasn't found.
273 		bool detach(T)()
274 		{
275 			auto info = getUDAs!(T, ComponentInfoParams)[0];
276 			return detach(ComponentInfo(info, typeid(T)));
277 		}
278 
279 		/// ditto
280 		bool detach(ComponentInfo info)
281 		{
282 			foreach (i, com; instanceComponents)
283 				if (com.info.name == info.name)
284 				{
285 					instanceComponents = instanceComponents.remove(i);
286 					com.wrapper.shutdown(false);
287 					return true;
288 				}
289 			return false;
290 		}
291 
292 		/// Loads a registered component which didn't have auto register on just for this instance.
293 		/// Returns: false instead of using the onBindFail callback on failure.
294 		/// Throws: Exception if component was not registered in workspaced.
295 		bool attach(T)(WorkspaceD workspaced)
296 		{
297 			string info = getUDAs!(T, ComponentInfoParams)[0];
298 			return attach(workspaced, ComponentInfo(info, typeid(T)));
299 		}
300 
301 		/// ditto
302 		bool attach(WorkspaceD workspaced, ComponentInfo info)
303 		{
304 			foreach (factory; workspaced.components)
305 			{
306 				if (factory.info.name == info.name)
307 				{
308 					Exception e;
309 					auto inst = factory.create(workspaced, this, e);
310 					if (inst)
311 					{
312 						attachComponent(ComponentWrapperInstance(inst, info));
313 						return true;
314 					}
315 					else
316 						return false;
317 				}
318 			}
319 			throw new Exception("Component not found");
320 		}
321 
322 		void attachComponent(ComponentWrapperInstance component)
323 		{
324 			instanceComponents ~= component;
325 		}
326 	}
327 
328 	import std.typecons : BlackHole;
329 	/// Called from components to push messages to the app.
330 	IMessageHandler messageHandler = new BlackHole!IMessageHandler;
331 	/// Called when ComponentFactory.create is called and errored (when the .bind call on a component fails)
332 	/// See_Also: $(LREF ComponentBindFailCallback)
333 	ComponentBindFailCallback onBindFail;
334 
335 	Instance[] instances;
336 	/// Base global configuration for new instances, does not modify existing ones.
337 	Configuration globalConfiguration;
338 	ComponentWrapperInstance[] globalComponents;
339 	ComponentFactoryInstance[] components;
340 	StringCache stringCache;
341 
342 	TaskPool _gthreads;
343 
344 	this()
345 	{
346 		stringCache = StringCache(StringCache.defaultBucketCount * 4);
347 	}
348 
349 	~this()
350 	{
351 		shutdown(true);
352 	}
353 
354 	void shutdown(bool dtor = false)
355 	{
356 		foreach (ref instance; instances)
357 			instance.shutdown(dtor);
358 		instances = null;
359 		foreach (ref com; globalComponents)
360 			com.wrapper.shutdown(dtor);
361 		globalComponents = null;
362 		components = null;
363 		if (_gthreads)
364 			_gthreads.finish(true);
365 		_gthreads = null;
366 	}
367 
368 	Instance getInstance(string cwd) nothrow
369 	{
370 		cwd = buildNormalizedPath(cwd);
371 		foreach (instance; instances)
372 			if (instance.cwd == cwd)
373 				return instance;
374 		return null;
375 	}
376 
377 	Instance getBestInstanceByDependency(WithComponent)(string file) nothrow
378 	{
379 		Instance best;
380 		size_t bestLength;
381 		foreach (instance; instances)
382 		{
383 			foreach (folder; chain(instance.importPaths, instance.importFiles,
384 					instance.stringImportPaths))
385 			{
386 				if (folder.length > bestLength && file.startsWith(folder)
387 						&& instance.has!WithComponent)
388 				{
389 					best = instance;
390 					bestLength = folder.length;
391 				}
392 			}
393 		}
394 		return best;
395 	}
396 
397 	Instance getBestInstanceByDependency(string file) nothrow
398 	{
399 		Instance best;
400 		size_t bestLength;
401 		foreach (instance; instances)
402 		{
403 			foreach (folder; chain(instance.importPaths, instance.importFiles,
404 					instance.stringImportPaths))
405 			{
406 				if (folder.length > bestLength && file.startsWith(folder))
407 				{
408 					best = instance;
409 					bestLength = folder.length;
410 				}
411 			}
412 		}
413 		return best;
414 	}
415 
416 	Instance getBestInstance(WithComponent)(string file, bool fallback = true) nothrow
417 	{
418 		file = buildNormalizedPath(file);
419 		Instance ret = null;
420 		size_t best;
421 		foreach (instance; instances)
422 		{
423 			if (instance.cwd.length > best && file.startsWith(instance.cwd)
424 					&& instance.has!WithComponent)
425 			{
426 				ret = instance;
427 				best = instance.cwd.length;
428 			}
429 		}
430 		if (!ret && fallback)
431 		{
432 			ret = getBestInstanceByDependency!WithComponent(file);
433 			if (ret)
434 				return ret;
435 			foreach (instance; instances)
436 				if (instance.has!WithComponent)
437 					return instance;
438 		}
439 		return ret;
440 	}
441 
442 	Instance getBestInstance(string file, bool fallback = true) nothrow
443 	{
444 		file = buildNormalizedPath(file);
445 		Instance ret = null;
446 		size_t best;
447 		foreach (instance; instances)
448 		{
449 			if (instance.cwd.length > best && file.startsWith(instance.cwd))
450 			{
451 				ret = instance;
452 				best = instance.cwd.length;
453 			}
454 		}
455 		if (!ret && fallback && instances.length)
456 		{
457 			ret = getBestInstanceByDependency(file);
458 			if (!ret)
459 				ret = instances[0];
460 		}
461 		return ret;
462 	}
463 
464 	/* virtual */
465 	void onBeforeAccessGlobalComponent(ComponentInfo) const
466 	{
467 	}
468 
469 	/* virtual */
470 	bool checkHasGlobalComponent(ComponentInfo info) const
471 	{
472 		foreach (com; globalComponents)
473 			if (com.info.name == info.name)
474 				return true;
475 		return false;
476 	}
477 
478 	T get(T)()
479 	{
480 		auto info = getUDAs!(T, ComponentInfoParams)[0];
481 		onBeforeAccessGlobalComponent(ComponentInfo(info, typeid(T)));
482 		foreach (com; globalComponents)
483 			if (com.info.name == info.name)
484 				return cast(T) com.wrapper;
485 		throw new Exception("Attempted to get unknown global component " ~ T.stringof);
486 	}
487 
488 	bool has(T)()
489 	{
490 		auto info = getUDAs!(T, ComponentInfoParams)[0];
491 		return checkHasGlobalComponent(ComponentInfo(info, typeid(T)));
492 	}
493 
494 	T get(T)(string cwd)
495 	{
496 		if (!cwd.length)
497 			return this.get!T;
498 		auto inst = getInstance(cwd);
499 		if (inst is null)
500 			throw new Exception("cwd '" ~ cwd ~ "' not found");
501 		return inst.get!T;
502 	}
503 
504 	bool has(T)(string cwd)
505 	{
506 		auto inst = getInstance(cwd);
507 		if (inst is null)
508 			return false;
509 		return inst.has!T;
510 	}
511 
512 	T best(T)(string file, bool fallback = true)
513 	{
514 		if (!file.length)
515 			return this.get!T;
516 		auto inst = getBestInstance!T(file);
517 		if (inst is null)
518 			throw new Exception("cwd for '" ~ file ~ "' not found");
519 		return inst.get!T;
520 	}
521 
522 	bool hasBest(T)(string cwd, bool fallback = true)
523 	{
524 		auto inst = getBestInstance!T(cwd);
525 		if (inst is null)
526 			return false;
527 		return inst.has!T;
528 	}
529 
530 	void onRegisterComponent(ref ComponentFactory factory, bool autoRegister)
531 	{
532 		components ~= ComponentFactoryInstance(factory, autoRegister);
533 		auto info = factory.info;
534 		Exception error;
535 		auto glob = factory.create(this, null, error);
536 		if (glob)
537 			globalComponents ~= ComponentWrapperInstance(glob, info);
538 		else if (onBindFail)
539 			onBindFail(null, factory, error);
540 
541 		if (autoRegister)
542 			foreach (ref instance; instances)
543 			{
544 				auto inst = factory.create(this, instance, error);
545 				if (inst)
546 					instance.attachComponent(ComponentWrapperInstance(inst, info));
547 				else if (onBindFail)
548 					onBindFail(instance, factory, error);
549 			}
550 	}
551 
552 	ComponentFactory register(T)(bool autoRegister = true)
553 	{
554 		ComponentFactory factory;
555 		static foreach (attr; __traits(getAttributes, T))
556 			static if (is(attr == class) && is(attr : ComponentFactory))
557 				factory = new attr;
558 		if (factory is null)
559 			factory = new DefaultComponentFactory!T;
560 
561 		onRegisterComponent(factory, autoRegister);
562 
563 		static if (__traits(compiles, T.registered(this)))
564 			T.registered(this);
565 		else static if (__traits(compiles, T.registered()))
566 			T.registered();
567 		return factory;
568 	}
569 
570 	protected Instance createInstance(string cwd, Configuration config)
571 	{
572 		auto inst = new Instance();
573 		inst.cwd = cwd;
574 		inst.config = config;
575 		return inst;
576 	}
577 
578 	protected void preloadComponents(Instance inst, string[] preloadComponents)
579 	{
580 		foreach (name; preloadComponents)
581 		{
582 			foreach (factory; components)
583 			{
584 				if (!factory.autoRegister && factory.info.name == name)
585 				{
586 					Exception error;
587 					auto wrap = factory.create(this, inst, error);
588 					if (wrap)
589 						inst.attachComponent(ComponentWrapperInstance(wrap, factory.info));
590 					else if (onBindFail)
591 						onBindFail(inst, factory, error);
592 					break;
593 				}
594 			}
595 		}
596 	}
597 
598 	protected void autoRegisterComponents(Instance inst)
599 	{
600 		foreach (factory; components)
601 		{
602 			if (factory.autoRegister)
603 			{
604 				Exception error;
605 				auto wrap = factory.create(this, inst, error);
606 				if (wrap)
607 					inst.attachComponent(ComponentWrapperInstance(wrap, factory.info));
608 				else if (onBindFail)
609 					onBindFail(inst, factory, error);
610 			}
611 		}
612 	}
613 
614 	/// Creates a new workspace with the given cwd with optional config overrides and preload components for non-autoRegister components.
615 	/// Throws: Exception if normalized cwd already exists as instance.
616 	Instance addInstance(string cwd, Configuration configOverrides = Configuration.none,
617 			string[] preloadComponents = [])
618 	{
619 		cwd = buildNormalizedPath(cwd);
620 		if (instances.canFind!(a => a.cwd == cwd))
621 			throw new Exception("Instance with cwd '" ~ cwd ~ "' already exists!");
622 		configOverrides.loadBase(globalConfiguration);
623 		auto inst = createInstance(cwd, configOverrides);
624 		this.preloadComponents(inst, preloadComponents);
625 		this.autoRegisterComponents(inst);
626 		instances ~= inst;
627 		return inst;
628 	}
629 
630 	bool removeInstance(string cwd)
631 	{
632 		cwd = buildNormalizedPath(cwd);
633 		foreach (i, instance; instances)
634 			if (instance.cwd == cwd)
635 			{
636 				foreach (com; instance.instanceComponents)
637 					destroy(com.wrapper);
638 				destroy(instance);
639 				instances = instances.remove(i);
640 				return true;
641 			}
642 		return false;
643 	}
644 
645 	deprecated("Use overload taking an out Exception error or attachSilent instead")
646 	final bool attach(Instance instance, string component)
647 	{
648 		return attachSilent(instance, component);
649 	}
650 
651 	final bool attachSilent(Instance instance, string component)
652 	{
653 		Exception error;
654 		return attach(instance, component, error);
655 	}
656 
657 	bool attach(Instance instance, string component, out Exception error)
658 	{
659 		foreach (factory; components)
660 		{
661 			if (factory.info.name == component)
662 			{
663 				auto wrap = factory.create(this, instance, error);
664 				if (wrap)
665 				{
666 					instance.attachComponent(ComponentWrapperInstance(wrap, factory.info));
667 					return true;
668 				}
669 				else
670 					return false;
671 			}
672 		}
673 		return false;
674 	}
675 
676 	TaskPool gthreads()
677 	{
678 		if (!_gthreads)
679 			synchronized (this)
680 				if (!_gthreads)
681 				{
682 					_gthreads = new TaskPool(max(2, min(6, defaultPoolThreads)));
683 					_gthreads.isDaemon = true;
684 				}
685 		return _gthreads;
686 	}
687 }
688 
689 version (unittest)
690 {
691 	struct TestingWorkspace
692 	{
693 		string directory;
694 
695 		@disable this(this);
696 
697 		this(string path)
698 		{
699 			if (path.exists)
700 				throw new Exception("Path already exists");
701 			directory = path;
702 			mkdir(path);
703 		}
704 
705 		~this()
706 		{
707 			rmdirRecurse(directory);
708 		}
709 
710 		string getPath(string path)
711 		{
712 			return buildPath(directory, path);
713 		}
714 
715 		void createDir(string dir)
716 		{
717 			mkdirRecurse(getPath(dir));
718 		}
719 
720 		void writeFile(string path, string content)
721 		{
722 			write(getPath(path), content);
723 		}
724 	}
725 
726 	TestingWorkspace makeTemporaryTestingWorkspace()
727 	{
728 		import std.random;
729 
730 		return TestingWorkspace(buildPath(tempDir,
731 				"workspace-d-test-" ~ uniform(0, long.max).to!string(36)));
732 	}
733 }