1 module rest.apiv1; 2 3 import vibe.d; 4 import rest.iapiv1; 5 import exec.iexecprovider; 6 import contentprovider; 7 8 class ApiV1: IApiV1 9 { 10 private IExecProvider execProvider_; 11 private ContentProvider contentProvider_; 12 string githubToken; /// Token for authenticating GitHub requests 13 14 this(IExecProvider execProvider, ContentProvider contentProvider, string githubToken) 15 { 16 this.execProvider_ = execProvider; 17 this.contentProvider_ = contentProvider; 18 this.githubToken = "token " ~ githubToken; 19 } 20 21 /++ 22 Parses the message contained in $(D output) 23 and fills the appropriate errors and warnings 24 arrays. 25 Note: parsing s just done when $(D output.success) 26 is false. 27 +/ 28 private static void parseErrorsAndWarnings(ref RunOutput output) 29 { 30 import std.regex: ctRegex, matchFirst, replaceAll; 31 import std.conv: to; 32 import std..string: lineSplitter; 33 34 static coloring = ctRegex! 35 `\x1B\[[0-9;]*[mGK]`; 36 static ctr = ctRegex! 37 `^[^(]+\(([0-9]+)(,[0-9]+)?\): ([a-zA-Z]+): (.*)$`; 38 39 foreach(line; output.output.replaceAll(coloring, "").lineSplitter) { 40 auto m = line.matchFirst(ctr); 41 if (m.empty) 42 continue; 43 auto lineNumber = to!int(m[1]); 44 string type = m[3]; 45 string message = m[4]; 46 47 switch (type) { 48 case "Warning": 49 case "Deprecation": 50 output.warnings ~= RunOutput.Message(lineNumber, 51 message); 52 break; 53 default: 54 output.errors ~= RunOutput.Message(lineNumber, 55 message); 56 } 57 } 58 } 59 60 // --- Argumentation for the chosen limit --- 61 // The average line length of Phobos std/ is about 30 characters, 62 // while on average files are 1800 lines long. The average file 63 // size is 62391 bytes. 64 // We pick a rather generous limit of 64k (64 * 1024) which ought 65 // to be enough for everybody. 66 enum uint maxSourceCodeLength = 64 * 1024; 67 68 RunOutput run(ApiV1.RunInput input) 69 { 70 if (input.source.length > maxSourceCodeLength) { 71 return RunOutput("ERROR: source code size is above limit of 64k bytes.", false); 72 } 73 74 // sanitize input and remove weird unicode characters 75 input.source = input.source.removeUnicodeSpaces; 76 77 // Be explicit here, in case the exec API and the REST API change 78 IExecProvider.RunInput runInput = { 79 source: input.source, 80 compiler : input.compiler, 81 args: input.args, 82 stdin: input.stdin, 83 color: input.color, 84 }; 85 auto result = execProvider_.compileAndExecute(runInput); 86 auto output = RunOutput(result.output, result.success); 87 parseErrorsAndWarnings(output); 88 return output; 89 } 90 91 FormatOutput format(string source) 92 { 93 // https://github.com/dlang-community/dfmt/blob/master/src/dfmt/config.d 94 import dfmt.config : Config; 95 import dfmt.formatter : format; 96 import std.array : appender; 97 import std.range.primitives; 98 99 FormatOutput output; 100 ubyte[] buffer; 101 buffer = cast(ubyte[]) source; 102 103 Config formatterConfig; 104 formatterConfig.initializeWithDefaults(); 105 auto app = appender!string; 106 format(source, buffer, app, &formatterConfig); 107 output.source = app.data; 108 if (output.source.back == '\n') 109 output.source.popBack; 110 return output; 111 } 112 113 ShortenOutput shorten(string source, string compiler, string args) 114 { 115 import std.format : format; 116 import std.uri : encodeComponent; 117 118 ShortenOutput output; 119 auto url = "https://run.dlang.io?compiler=%s&source=%s".format(compiler, source.encodeComponent); 120 if (args.length > 0) 121 url ~= "&args=" ~ args.encodeComponent; 122 123 auto isURL= "https://is.gd/create.php?format=simple&url=%s".format(url.encodeComponent); 124 output.url = requestHTTP(isURL, (scope req) { 125 req.method = HTTPMethod.POST; 126 }).bodyReader.readAllUTF8.replace("https://is.gd/", "https://run.dlang.io/is/"); 127 output.success = true; 128 return output; 129 } 130 131 GistOutput gist(string source, string compiler, string args) 132 { 133 import std.format : format; 134 import std.uri : encodeComponent; 135 136 GistOutput output; 137 138 auto res = requestHTTP("https://api.github.com/gists", (scope req) { 139 req.headers["Authorization"] = githubToken; 140 Json data = Json.emptyObject; 141 data["description"] = "Code shared from run.dlang.io.%s".format(args ? 142 " Run with '%s'".format(args) : ""); 143 data["public"] = true; 144 Json files = Json.emptyObject; 145 files["main.d"] = Json([ 146 "content": Json(source), 147 ]); 148 data["files"] = files; 149 req.method = HTTPMethod.POST; 150 req.writeJsonBody(data); 151 }); 152 enforceHTTP(res.statusCode / 100 == 2, HTTPStatus.internalServerError, "GitHub API not available."); 153 auto json = res.readJson; 154 auto gistId = json["id"].get!string; 155 logInfo("Gist created: %s", gistId); 156 output.id = gistId; 157 output.htmlUrl = json["html_url"].get!string; 158 159 output.url = "/gist/" ~ gistId; 160 if (compiler != "dmd") { 161 output.url ~= "?compiler=%s".format(compiler); 162 if (args.length > 0) 163 output.url ~= "&args=" ~ args.encodeComponent; 164 } else if (args.length > 0) { 165 output.url ~= "?args=" ~ args.encodeComponent; 166 } 167 168 return output; 169 } 170 171 unittest 172 { 173 string source = `void main() {}`; 174 ApiV1 api = new ApiV1(null, null, null); 175 auto res = api.format(source); 176 assert(res == FormatOutput("void main()\n{\n}", false)); 177 } 178 179 SourceOutput getSource(string _language, string _chapter, string _section) 180 { 181 auto tourData = contentProvider_.getContent(_language, _chapter, _section); 182 if (tourData.content == null) { 183 throw new HTTPStatusException(404, 184 "Couldn't find tour data for chapter '%s', section %d".format(_language, _chapter, _section)); 185 } 186 187 return SourceOutput(tourData.content.sourceCode); 188 } 189 } 190 191 unittest { 192 string run1 = `onlineapp.d(6): Error: found '}' when expecting ';' following statement 193 onlineapp.d(6): Error: found 'EOF' when expecting '}' following compound statement 194 Failed: ["dmd", "-v", "-o-", "onlineapp.d", "-I."]`; 195 196 auto test2 = ApiV1.RunOutput(run1, false); 197 ApiV1.parseErrorsAndWarnings(test2); 198 assert(test2.errors.length == 2); 199 assert(test2.errors[0].line == 6); 200 assert(test2.errors[0].message[0 .. 9] == "found '}'"); 201 assert(test2.errors[1].line == 6); 202 assert(test2.errors[1].message[0 .. 11] == "found 'EOF'"); 203 204 string run2 = `dlang-tour ~master: building configuration "executable"... 205 ../.dub/packages/dyaml-0.5.2/source/dyaml/dumper.d(15,8): Deprecation: module std.stream is deprecated - It will be removed from Phobos in October 2016. If you still need it, go to https://github.com/DigitalMars/undeaD 206 ../.dub/packages/dyaml-0.5.2/source/dyaml/emitter.d(21,8): Deprecation: module std.stream is deprecated - It will be removed from Phobos in October 2016. If you still need it, go to https://github.com/DigitalMars/undeaD 207 ../.dub/packages/dyaml-0.5.2/source/dyaml/representer.d(679,8): Deprecation: module std.stream is deprecated - It will be removed from Phobos in October 2016. If you still need it, go to https://github.com/DigitalMars/undeaD 208 ../.dub/packages/dyaml-0.5.2/source/dyaml/loader.d(171,16): Deprecation: module std.stream is deprecated - It will be removed from Phobos in October 2016. If you still need it, go to https://github.com/DigitalMars/undeaD`; 209 auto test3 = ApiV1.RunOutput(run2, false); 210 ApiV1.parseErrorsAndWarnings(test3); 211 assert(test3.warnings.length == 4); 212 import std.algorithm: all; 213 assert(test3.warnings.all!(x => x.message[0 .. 31] == "module std.stream is deprecated")); 214 assert(test3.warnings[0].line == 15); 215 assert(test3.warnings[1].line == 21); 216 assert(test3.warnings[2].line == 679); 217 assert(test3.warnings[3].line == 171); 218 219 auto run3 = 220 "\u001B[1md.d(1): \u001B[1;31mError: \u001B[mbasic type expected, not {\n" ~ 221 "\u001B[1md.d(1): \u001B[1;31mError: \u001B[mfound '{' when expecting ')'\n" ~ 222 "\u001B[1md.d(1): \u001B[1;31mError: \u001B[msemicolon expected following function declaration\n" ~ 223 "\u001B[1md.d(1): \u001B[1;31mError: \u001B[munrecognized declaration\n"; 224 225 auto test4 = ApiV1.RunOutput(run3, false); 226 ApiV1.parseErrorsAndWarnings(test4); 227 228 assert(test4.errors.length == 4); 229 assert(test4.errors[0] == 230 ApiV1.RunOutput.Message(1, "basic type expected, not {")); 231 assert(test4.errors[1] == 232 ApiV1.RunOutput.Message(1, "found '{' when expecting ')'")); 233 assert(test4.errors[2] == 234 ApiV1.RunOutput.Message(1, "semicolon expected following function declaration")); 235 assert(test4.errors[3] == 236 ApiV1.RunOutput.Message(1, "unrecognized declaration")); 237 } 238 239 // remove weird unicode spaces 240 private string removeUnicodeSpaces(S)(S input) { 241 import std.algorithm.iteration : map; 242 import std.array : array; 243 import std.uni : isSpace; 244 // selective import will trigger false-positive deprecation warning 245 import std.utf; 246 return (cast(dstring) input.map!(c => c.isSpace ? ' ' : c).array).toUTF8; 247 } 248 249 // weird 0xa0 unicode space tokens on android 250 unittest { 251 import std..string : representation; 252 ubyte[] helloWorldNormal = [10, 32, 32, 32, 32, 32, 32, 119, 114, 105]; 253 ubyte[] helloWorldAndroid = [10, 32, 194, 160, 32, 194, 160, 32, 32, 119, 114, 105]; 254 string res = cast(string) helloWorldAndroid; 255 assert(res.removeUnicodeSpaces.representation == helloWorldNormal); 256 }