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