Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix some rest js client problems #1566

Merged
merged 12 commits into from
Sep 23, 2016
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