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