Skip to content

Commit

Permalink
Merge pull request #1566 from deviator/master
Browse files Browse the repository at this point in the history
Fix some rest js client problems. (#1564, #1565)
  • Loading branch information
s-ludwig committed Sep 23, 2016
2 parents 10e37ed + 9974d5c commit e5e557a
Show file tree
Hide file tree
Showing 2 changed files with 200 additions and 43 deletions.
221 changes: 186 additions & 35 deletions source/vibe/web/internal/rest/jsclient.d
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,85 +59,105 @@ 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) {
alias FT = FunctionTypeOf!F;
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) {
Expand Down Expand Up @@ -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));
}
22 changes: 14 additions & 8 deletions source/vibe/web/rest.d
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

///
Expand All @@ -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);
Expand All @@ -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');")
*/
Expand All @@ -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.
Expand All @@ -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();
}


Expand Down

0 comments on commit e5e557a

Please sign in to comment.