diff --git a/source/vibe/web/internal/rest/jsclient.d b/source/vibe/web/internal/rest/jsclient.d index 19eff4734b..b9501180c1 100644 --- a/source/vibe/web/internal/rest/jsclient.d +++ b/source/vibe/web/internal/rest/jsclient.d @@ -12,17 +12,45 @@ import vibe.web.rest; import std.conv : to; +/// +class JSRestClientGenerateSettings +{ + /// + string indentStep; + /// + string name; + /// + bool parent; + + /// + this(string indentStep=" ", string name=null, bool parent=true) + { + this.name = name; + this.parent = parent; + this.indentStep = indentStep; + } + + auto child(string cname) + { + return new JSRestClientGenerateSettings(indentStep, cname, false); + } +} /** Generates JavaScript code suitable for accessing a REST interface using XHR. */ -/*package(vibe.web.web)*/ void generateInterface(TImpl, R)(ref R output, string name, RestInterfaceSettings settings) +/*package(vibe.web.web)*/ void generateInterface(TImpl, R)(ref R output, RestInterfaceSettings settings, + JSRestClientGenerateSettings jsgenset) { // TODO: handle attributed parameters and filter out internal parameters that have no path placeholder assigned to them import std.format : formattedWrite; - import std.string : toUpper; + import std.string : toUpper, strip, splitLines; import std.traits : FunctionTypeOf, ReturnType; + import std.algorithm : filter, map; + import std.array : replace; + import std.typecons : tuple; + import vibe.data.json : Json, serializeToJson; import vibe.internal.meta.uda; import vibe.http.common : HTTPMethod; @@ -31,12 +59,29 @@ import std.conv : to; auto intf = RestInterface!TImpl(settings, true); - output.formattedWrite("%s = new function() {\n", name.length ? name : intf.I.stringof); + auto fout = indentSink(output, jsgenset.indentStep); + + fout.formattedWrite("%s%s = new function() {\n", jsgenset.parent ? "" : "this.", + jsgenset.name.length ? jsgenset.name : intf.I.stringof); - output.put("var toRestString = function(v) { return v; }\n"); + if (jsgenset.parent) { + auto lns = ` + var toRestString = function(v) { + var res; + switch(typeof(v)) { + case "object": res = JSON.stringify(v); break; + default: res = v; + } + return encodeURIComponent(res); + }`; + foreach(ln; lns.splitLines.map!(a=>a.strip ~ "\n")) + fout.put(ln); + } foreach (i, SI; intf.SubInterfaceTypes) { - output.generateInterface!SI(__traits(identifier, intf.SubInterfaceFunctions[i]), intf.subInterfaces[i].settings); + fout.put("\n"); + auto childjsset = jsgenset.child(__traits(identifier, intf.SubInterfaceFunctions[i])); + fout.generateInterface!SI(intf.subInterfaces[i].settings, childjsset); } foreach (i, F; intf.RouteFunctions) { @@ -44,72 +89,75 @@ import std.conv : to; auto route = intf.routes[i]; // function signature - output.formattedWrite(" this.%s = function(", route.functionName); + fout.put("\n"); + fout.formattedWrite("this.%s = function(", route.functionName); foreach (j, param; route.parameters) { - output.put(param.name); - output.put(", "); + fout.put(param.name); + fout.put(", "); } - static if (!is(ReturnType!FT == void)) output.put("on_result, "); - output.put("on_error) {\n"); + static if (!is(ReturnType!FT == void)) fout.put("on_result, "); + + fout.put("on_error) {\n"); // url assembly + fout.put("var url = "); if (route.pathHasPlaceholders) { // extract the server part of the URL - output.put(" var url = "); auto burl = URL(intf.baseURL); burl.pathString = "/"; - output.serializeToJson(burl.toString()[0 .. $-1]); + fout.serializeToJson(burl.toString()[0 .. $-1]); // and then assemble the full path piece-wise foreach (p; route.fullPathParts) { - output.put(" + "); - if (!p.isParameter) output.serializeToJson(p.text); - else output.formattedWrite("encodeURIComponent(toRestString(%s))", p.text); + fout.put(" + "); + if (!p.isParameter) fout.serializeToJson(p.text); + else fout.formattedWrite("toRestString(%s)", p.text); } - output.put(";\n"); } else { - output.formattedWrite(" var url = %s;\n", Json(concatURL(intf.baseURL, route.pattern))); + fout.formattedWrite(`"%s"`, concatURL(intf.baseURL, route.pattern)); } + fout.put(";\n"); // query parameters if (route.queryParameters.length) { - output.put(" url = url"); + fout.put("url = url"); foreach (j, p; route.queryParameters) - output.formattedWrite(" + \"%s%s=\" + encodeURIComponent(toRestString(%s))", + fout.formattedWrite(" + \"%s%s=\" + toRestString(%s)", j == 0 ? '?' : '&', p.fieldName, p.name); - output.put(";\n"); + fout.put(";\n"); } // body parameters if (route.bodyParameters.length) { - output.put(" var postbody = {\n"); + fout.put("var postbody = {\n"); foreach (p; route.bodyParameters) - output.formattedWrite(" %s: toRestString(%s),\n", Json(p.fieldName), p.name); - output.put(" };\n"); + fout.formattedWrite("%s: %s,\n", Json(p.fieldName), p.name); + fout.put("};\n"); } // XHR setup - output.put(" var xhr = new XMLHttpRequest();\n"); - output.formattedWrite(" xhr.open('%s', url, true);\n", route.method.to!string.toUpper); + fout.put("var xhr = new XMLHttpRequest();\n"); + fout.formattedWrite("xhr.open('%s', url, true);\n", route.method.to!string.toUpper); static if (!is(ReturnType!FT == void)) { - output.put(" xhr.onload = function () { if (this.status >= 400) { if (on_error) on_error(JSON.parse(this.responseText)); else console.log(this.responseText); } else on_result(JSON.parse(this.responseText)); };\n"); + fout.put("xhr.onload = function () {\n"); + fout.put("if (this.status >= 400) { if (on_error) on_error(JSON.parse(this.responseText)); else console.log(this.responseText); }\n"); + fout.put("else on_result(JSON.parse(this.responseText));\n"); + fout.put("};\n"); } // header parameters foreach (p; route.headerParameters) - output.formattedWrite(" xhr.setRequestHeader(%s, %s);\n", Json(p.fieldName), p.name); + fout.formattedWrite("xhr.setRequestHeader(%s, %s);\n", Json(p.fieldName), p.name); // submit request if (route.method == HTTPMethod.GET || !route.bodyParameters.length) - output.put(" xhr.send();\n"); + fout.put("xhr.send();\n"); else { - output.put(" xhr.setRequestHeader('Content-Type', 'application/json');\n"); - output.put(" xhr.send(JSON.stringify(postbody));\n"); + fout.put("xhr.setRequestHeader('Content-Type', 'application/json');\n"); + fout.put("xhr.send(JSON.stringify(postbody));\n"); } - - output.put(" }\n\n"); + fout.put("}\n"); } - - output.put("}\n"); + fout.put("}\n"); } version (unittest) { @@ -140,9 +188,112 @@ unittest { // issue #1293 auto settings = new RestInterfaceSettings; settings.baseURL = URL("http://localhost/"); auto app = appender!string(); - app.generateInterface!I(null, settings); + auto jsgenset = new JSRestClientGenerateSettings; + app.generateInterface!I(settings, jsgenset); + assert(app.data.canFind("this.s = new function()")); assert(app.data.canFind("this.test1 = function(on_result, on_error)")); assert(app.data.find("this.test1 = function").canFind("xhr.onload =")); assert(app.data.canFind("this.test2 = function(on_error)")); assert(!app.data.find("this.test2 = function").canFind("xhr.onload =")); } + +private auto indentSink(O)(ref O output, string step) +{ + static struct IndentSink(R) + { + import std.string : strip; + import std.algorithm : joiner; + import std.range : repeat; + + R* base; + string indent; + size_t level, tempLevel; + alias orig this; + + this(R* base, string indent) + { + this.base = base; + this.indent = indent; + } + + void pushIndent() + { + level++; + tempLevel++; + } + + void popIndent() + { + if (!level) return; + + level--; + if (tempLevel) + tempLevel = level; + } + + void postPut(const(char)[] s) + { + auto ss = s.strip; + if (ss.length && ss[$-1] == '{') + pushIndent(); + + if (s.length && s[$-1] == '\n') + tempLevel = level; + } + + void prePut(const(char)[] s) + { + auto ss = s.strip; + if (ss.length && ss[0] == '}') + popIndent(); + + orig.put(indent.repeat(tempLevel).joiner()); + tempLevel = 0; + } + + void put(const(char)[] s) { prePut(s); orig.put(s); postPut(s); } + + void put(char c) { prePut([c]); orig.put(c); postPut([c]); } + + void formattedWrite(Args...)(string fmt, Args args) + { + import std.format : formattedWrite; + + prePut(fmt); + orig.formattedWrite(fmt, args); + postPut(fmt); + } + + ref R orig() @property { return *base; } + } + + static if (is(typeof(output.prePut)) && is(typeof(output.postPut))) // is IndentSink + return output; + else + return IndentSink!O(&output, step); +} + +unittest { + import std.array : appender; + import std.format : formattedWrite; + import std.algorithm : equal; + + auto buf = appender!string(); + auto ind = indentSink(buf, "\t"); + ind.put("class A {\n"); + ind.put("int func() { return 12; }\n"); + + auto ind2 = indentSink(ind, " "); // return itself, not override indentStep + + ind2.formattedWrite("void %s(%-(%s, %)) {\n", "func2", ["int a", "float b", "char c"]); + ind2.formattedWrite("if (%s == %s) {\n", "a", "0"); + ind2.put("action();\n"); + ind2.put("}\n"); + ind2.put("}\n"); + ind.put("}\n"); + + auto res = "class A {\n\tint func() { return 12; }\n\tvoid func2(int a, float b, char c) {\n\t\t" ~ + "if (a == 0) {\n\t\t\taction();\n\t\t}\n\t}\n}\n"; + + assert(equal(res, buf.data)); +} diff --git a/source/vibe/web/rest.d b/source/vibe/web/rest.d index 733157ec76..688232a6fd 100644 --- a/source/vibe/web/rest.d +++ b/source/vibe/web/rest.d @@ -243,14 +243,14 @@ HTTPServerRequestDelegate serveRestJSClient(I)(URL base_url) { auto settings = new RestInterfaceSettings; settings.baseURL = base_url; - return serveRestJSClient(settings); + return serveRestJSClient!I(settings); } /// ditto HTTPServerRequestDelegate serveRestJSClient(I)(string base_url) { auto settings = new RestInterfaceSettings; settings.baseURL = URL(base_url); - return serveRestJSClient(settings); + return serveRestJSClient!I(settings); } /// @@ -269,6 +269,8 @@ unittest { auto router = new URLRouter; router.get("/myapi.js", serveRestJSClient!MyAPI(restsettings)); + //router.get("/myapi.js", serveRestJSClient!MyAPI(URL("http://api.example.org/"))); + //router.get("/myapi.js", serveRestJSClient!MyAPI("http://api.example.org/")); //router.get("/", staticTemplate!"index.dt"); listenHTTP(new HTTPServerSettings, router); @@ -279,7 +281,7 @@ unittest { html head title JS REST client test - script(src="test.js") + script(src="myapi.js") body button(onclick="MyAPI.postBar('hello');") */ @@ -289,11 +291,12 @@ unittest { /** Generates JavaScript code to access a REST interface from the browser. */ -void generateRestJSClient(I, R)(ref R output, RestInterfaceSettings settings = null) +void generateRestJSClient(I, R)(ref R output, RestInterfaceSettings settings) if (is(I == interface) && isOutputRange!(R, char)) { - import vibe.web.internal.rest.jsclient : generateInterface; - output.generateInterface!I(null, settings); + import vibe.web.internal.rest.jsclient : generateInterface, JSRestClientGenerateSettings; + auto jsgenset = new JSRestClientGenerateSettings; + output.generateInterface!I(settings, jsgenset); } /// Writes a JavaScript REST client to a local .js file. @@ -310,9 +313,12 @@ unittest { import std.array : appender; auto app = appender!string; - generateRestJSClient!MyAPI(app); - writeFileUTF8(Path("myapi.js"), app.data); + auto settings = new RestInterfaceSettings; + settings.baseURL = URL("http://localhost/"); + generateRestJSClient!MyAPI(app, settings); } + + generateJSClientImpl(); }