1 module workspaced.api;
2 
3 import std.conv;
4 import std.json;
5 import std.file;
6 import std.path;
7 import std.regex;
8 import core.time;
9 import painlessjson;
10 import standardpaths;
11 
12 ///
13 alias AsyncCallback = void delegate(Throwable, JSONValue);
14 
15 /// Will get called asynchronously (Must prepend AsyncCallback as argument)
16 enum async = 2603248026;
17 
18 /// Will get called for loading components
19 enum load = 2603248027;
20 
21 /// Will get called for unloading components
22 enum unload = 2603248028;
23 
24 /// Will call this function in any case (cmd: component)
25 enum any = 2603248029;
26 
27 /// Will never call this function
28 enum disabledFunc = 2603248030;
29 
30 /// Component call
31 struct component
32 {
33 	/// Name of the component
34 	string name;
35 }
36 
37 /// Will get called when some argument matches
38 struct Arguments
39 {
40 	/// Arguments to match
41 	Argument[] arguments;
42 }
43 
44 private struct Argument
45 {
46 	/// Key in JSON object node at root level to match
47 	string key;
48 	/// Value in JSON object node at root level to match
49 	JSONValue value;
50 }
51 
52 private template ArgumentPair(size_t i)
53 {
54 	static if (i > 0)
55 		enum ArgumentPair = "ret.arguments[" ~ (i / 2 - 1)
56 				.to!string ~ "] = Argument(args[" ~ (i - 2).to!string ~ "], args[" ~ (i - 1)
57 				.to!string ~ "].toJSON);" ~ ArgumentPair!(i - 2);
58 	else
59 					enum ArgumentPair = "";
60 }
61 
62 package Arguments arguments(T...)(T args)
63 {
64 	if (args.length < 2)
65 		return Arguments.init;
66 	Arguments ret;
67 	ret.arguments.length = args.length / 2;
68 	mixin(ArgumentPair!(args.length));
69 	return ret;
70 }
71 
72 unittest
73 {
74 	Arguments args = arguments("foo", 5, "bar", "str");
75 	assert(args.arguments[0].key == "foo");
76 	assert(args.arguments[0].value.integer == 5);
77 	assert(args.arguments[1].key == "bar");
78 	assert(args.arguments[1].value.str == "str");
79 }
80 
81 /// Describes what to insert/replace/delete to do something
82 struct CodeReplacement
83 {
84 	/// Range what to replace. If both indices are the same its inserting.
85 	size_t[2] range;
86 	/// Content to replace it with. Empty means remove.
87 	string content;
88 
89 	/// Applies this edit to a string.
90 	string apply(string code)
91 	{
92 		size_t min = range[0];
93 		size_t max = range[1];
94 		if (min > max)
95 		{
96 			min = range[1];
97 			max = range[0];
98 		}
99 		if (min >= code.length)
100 			return code ~ content;
101 		if (max >= code.length)
102 			return code[0 .. min] ~ content;
103 		return code[0 .. min] ~ content ~ code[max .. $];
104 	}
105 }
106 
107 /// Code replacements mapped to a file
108 struct FileChanges
109 {
110 	/// File path to change.
111 	string file;
112 	/// Replacements to apply.
113 	CodeReplacement[] replacements;
114 }
115 
116 package bool getConfigPath(string file, ref string retPath)
117 {
118 	foreach (dir; standardPaths(StandardPath.config, "workspace-d"))
119 	{
120 		auto path = buildPath(dir, file);
121 		if (path.exists)
122 		{
123 			retPath = path;
124 			return true;
125 		}
126 	}
127 	return false;
128 }
129 
130 alias ImportPathProvider = string[]function();
131 
132 private string[] noImports()
133 {
134 	return [];
135 }
136 
137 ImportPathProvider importPathProvider = &noImports;
138 ImportPathProvider stringImportPathProvider = &noImports;
139 ImportPathProvider importFilesProvider = &noImports;
140 
141 enum verRegex = ctRegex!`(\d+)\.(\d+)\.(\d+)`;
142 bool checkVersion(string ver, int[3] target)
143 {
144 	auto match = ver.matchFirst(verRegex);
145 	assert(match);
146 	int major = match[1].to!int;
147 	int minor = match[2].to!int;
148 	int patch = match[3].to!int;
149 	if (major > target[0])
150 		return true;
151 	if (major == target[0] && minor >= target[1])
152 		return true;
153 	if (major == target[0] && minor == target[1] && patch >= target[2])
154 		return true;
155 	return false;
156 }
157 
158 alias BroadcastCallback = void function(JSONValue);
159 /// Broadcast callback which might get called by commands. For example when a component is outdated. Will be called in caller thread of function / while function executes.
160 BroadcastCallback broadcastCallback;
161 /// Broadcast callback which might get called by commands. This callback will get called by all threads.
162 __gshared BroadcastCallback crossThreadBroadcastCallback;
163 
164 package void broadcast(JSONValue value)
165 {
166 	if (broadcastCallback)
167 		broadcastCallback(value);
168 	if (crossThreadBroadcastCallback)
169 		crossThreadBroadcastCallback(value);
170 }
171 
172 package string getVersionAndFixPath(ref string execPath)
173 {
174 	import std.process;
175 
176 	try
177 	{
178 		return execute([execPath, "--version"]).output;
179 	}
180 	catch (ProcessException e)
181 	{
182 		auto newPath = buildPath(thisExePath.dirName, execPath.baseName);
183 		if (exists(newPath))
184 		{
185 			execPath = newPath;
186 			return execute([execPath, "--version"]).output;
187 		}
188 		throw e;
189 	}
190 }
191 
192 /// Calls an asynchronous function and blocks until it returns using Thread.sleep
193 JSONValue syncBlocking(alias fn, alias sleepDur = 1.msecs, Args...)(Args args)
194 {
195 	import core.thread;
196 
197 	Throwable ex;
198 	JSONValue ret;
199 	bool done = false;
200 	AsyncCallback cb = (err, data) { ex = err; ret = data; done = true; };
201 	fn(cb, args);
202 	while (!done)
203 		Thread.sleep(sleepDur);
204 	if (ex)
205 		throw ex;
206 	return ret;
207 }
208 
209 /// Calls an asynchronous function and blocks until it returns using Fiber.yield
210 JSONValue syncYield(alias fn, Args...)(Args args)
211 {
212 	import core.thread;
213 
214 	Throwable ex;
215 	JSONValue ret;
216 	bool done = false;
217 	AsyncCallback cb = (err, data) { ex = err; ret = data; done = true; };
218 	fn(cb, args);
219 	while (!done)
220 		Fiber.yield;
221 	if (ex)
222 		throw ex;
223 	return ret;
224 }
225 
226 version(unittest)
227 {
228 	struct TestingWorkspace
229 	{
230 		string directory;
231 
232 		this(string path)
233 		{
234 			if (path.exists)
235 				throw new Exception("Path already exists");
236 			directory = path;
237 			mkdir(path);
238 		}
239 
240 		~this()
241 		{
242 			rmdirRecurse(directory);
243 		}
244 
245 		string getPath(string path)
246 		{
247 			return buildPath(directory, path);
248 		}
249 
250 		void createDir(string dir)
251 		{
252 			mkdirRecurse(getPath(dir));
253 		}
254 
255 		void writeFile(string path, string content)
256 		{
257 			write(getPath(path), content);
258 		}
259 	}
260 
261 	TestingWorkspace makeTemporaryTestingWorkspace()
262 	{
263 		import std.random;
264 
265 		return TestingWorkspace(buildPath(tempDir, "workspace-d-test-" ~ uniform(0, int.max).to!string(36)));
266 	}
267 }