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 }