mirror of https://github.com/adamdruppe/arsd.git
prototype of web.d 2.0 in cgi.d
This commit is contained in:
parent
ec4a718730
commit
7bcd3eca8d
720
cgi.d
720
cgi.d
|
@ -612,6 +612,9 @@ class Cgi {
|
|||
|
||||
environmentVariables = cast(const) environment.toAA;
|
||||
|
||||
string[] allPostNamesInOrder;
|
||||
string[] allPostValuesInOrder;
|
||||
|
||||
foreach(arg; args[1 .. $]) {
|
||||
if(arg.startsWith("--")) {
|
||||
nextArgIs = arg[2 .. $];
|
||||
|
@ -694,6 +697,8 @@ class Cgi {
|
|||
if(requestMethod == Cgi.RequestMethod.POST) {
|
||||
auto parts = breakUp(arg);
|
||||
_post[parts[0]] ~= parts[1];
|
||||
allPostNamesInOrder ~= parts[0];
|
||||
allPostValuesInOrder ~= parts[1];
|
||||
} else {
|
||||
if(_queryString.length)
|
||||
_queryString ~= "&";
|
||||
|
@ -712,7 +717,7 @@ class Cgi {
|
|||
cookies = keepLastOf(cookiesArray);
|
||||
|
||||
queryString = _queryString;
|
||||
getArray = cast(immutable) decodeVariables(queryString);
|
||||
getArray = cast(immutable) decodeVariables(queryString, "&", &allGetNamesInOrder, &allGetValuesInOrder);
|
||||
get = keepLastOf(getArray);
|
||||
|
||||
postArray = cast(immutable) _post;
|
||||
|
@ -741,6 +746,13 @@ class Cgi {
|
|||
this.postJson = null;
|
||||
}
|
||||
|
||||
private {
|
||||
string[] allPostNamesInOrder;
|
||||
string[] allPostValuesInOrder;
|
||||
string[] allGetNamesInOrder;
|
||||
string[] allGetValuesInOrder;
|
||||
}
|
||||
|
||||
CgiConnectionHandle getOutputFileHandle() {
|
||||
return _outputFileHandle;
|
||||
}
|
||||
|
@ -842,7 +854,7 @@ class Cgi {
|
|||
}
|
||||
|
||||
|
||||
auto ugh = decodeVariables(queryString);
|
||||
auto ugh = decodeVariables(queryString, "&", &allGetNamesInOrder, &allGetValuesInOrder);
|
||||
getArray = assumeUnique(ugh);
|
||||
get = keepLastOf(getArray);
|
||||
|
||||
|
@ -1208,9 +1220,16 @@ class Cgi {
|
|||
// I used to not do it, but I think I should, since it is there...
|
||||
pps._post[pps.piece.name] ~= pps.piece.filename;
|
||||
pps._files[pps.piece.name] ~= pps.piece;
|
||||
} else
|
||||
|
||||
allPostNamesInOrder ~= pps.piece.name;
|
||||
allPostValuesInOrder ~= pps.piece.filename;
|
||||
} else {
|
||||
pps._post[pps.piece.name] ~= cast(string) pps.piece.content;
|
||||
|
||||
allPostNamesInOrder ~= pps.piece.name;
|
||||
allPostValuesInOrder ~= cast(string) pps.piece.content;
|
||||
}
|
||||
|
||||
/*
|
||||
stderr.writeln("RECEIVED: ", pps.piece.name, "=",
|
||||
pps.piece.content.length < 1000
|
||||
|
@ -1455,7 +1474,7 @@ class Cgi {
|
|||
if(pps.isJson)
|
||||
pps.postJson = cast(string) pps.buffer;
|
||||
else
|
||||
pps._post = decodeVariables(cast(string) pps.buffer);
|
||||
pps._post = decodeVariables(cast(string) pps.buffer, "&", &allPostNamesInOrder, &allPostValuesInOrder);
|
||||
version(preserveData)
|
||||
originalPostData = pps.buffer;
|
||||
} else {
|
||||
|
@ -1620,7 +1639,7 @@ class Cgi {
|
|||
pathInfo = requestUri[pathInfoStarts..question];
|
||||
}
|
||||
|
||||
auto ugh = decodeVariables(queryString);
|
||||
auto ugh = decodeVariables(queryString, "&", &allGetNamesInOrder, &allGetValuesInOrder);
|
||||
getArray = cast(string[][string]) assumeUnique(ugh);
|
||||
|
||||
if(header.indexOf("HTTP/1.0") != -1) {
|
||||
|
@ -2671,18 +2690,28 @@ struct Uri {
|
|||
*/
|
||||
|
||||
/// breaks down a url encoded string
|
||||
string[][string] decodeVariables(string data, string separator = "&") {
|
||||
string[][string] decodeVariables(string data, string separator = "&", string[]* namesInOrder = null, string[]* valuesInOrder = null) {
|
||||
auto vars = data.split(separator);
|
||||
string[][string] _get;
|
||||
foreach(var; vars) {
|
||||
auto equal = var.indexOf("=");
|
||||
string name;
|
||||
string value;
|
||||
if(equal == -1) {
|
||||
_get[decodeComponent(var)] ~= "";
|
||||
name = decodeComponent(var);
|
||||
value = "";
|
||||
} else {
|
||||
//_get[decodeComponent(var[0..equal])] ~= decodeComponent(var[equal + 1 .. $].replace("+", " "));
|
||||
// stupid + -> space conversion.
|
||||
_get[decodeComponent(var[0..equal].replace("+", " "))] ~= decodeComponent(var[equal + 1 .. $].replace("+", " "));
|
||||
name = decodeComponent(var[0..equal].replace("+", " "));
|
||||
value = decodeComponent(var[equal + 1 .. $].replace("+", " "));
|
||||
}
|
||||
|
||||
_get[name] ~= value;
|
||||
if(namesInOrder)
|
||||
(*namesInOrder) ~= name;
|
||||
if(valuesInOrder)
|
||||
(*valuesInOrder) ~= value;
|
||||
}
|
||||
return _get;
|
||||
}
|
||||
|
@ -5659,38 +5688,50 @@ ssize_t read_fd(int fd, void *ptr, size_t nbytes, int *recvfd) {
|
|||
switch to choose if you want to override.
|
||||
*/
|
||||
|
||||
/+
|
||||
struct StaticFile {
|
||||
string path;
|
||||
string file;
|
||||
// the following will be guessed automatically from the file type
|
||||
string contentType;
|
||||
bool gzip;
|
||||
bool cache;
|
||||
}
|
||||
|
||||
|
||||
with(cgi.urlDispatcher()) {
|
||||
|
||||
}
|
||||
+/
|
||||
struct DispatcherDefinition(alias dispatchHandler) {// if(is(typeof(dispatchHandler("str", Cgi.init) == bool))) { // bool delegate(string urlPrefix, Cgi cgi) dispatchHandler;
|
||||
alias handler = dispatchHandler;
|
||||
string urlPrefix;
|
||||
bool rejectFurther;
|
||||
}
|
||||
|
||||
struct CallableFromWeb {
|
||||
Cgi.RequestMethod httpMethod;
|
||||
string path;
|
||||
void delegate(Cgi cgi) callFromCgi;
|
||||
string[] parameters;
|
||||
}
|
||||
|
||||
private string urlify(string name) {
|
||||
return name;
|
||||
}
|
||||
|
||||
private string beautify(string name) {
|
||||
char[160] buffer;
|
||||
int bufferIndex = 0;
|
||||
bool shouldCap = true;
|
||||
bool shouldSpace;
|
||||
bool lastWasCap;
|
||||
foreach(idx, char ch; name) {
|
||||
if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
|
||||
|
||||
if(ch >= 'A' && ch <= 'Z') {
|
||||
if(lastWasCap) {
|
||||
// two caps in a row, don't change. Prolly acronym.
|
||||
} else {
|
||||
if(idx)
|
||||
shouldSpace = true; // new word, add space
|
||||
}
|
||||
|
||||
lastWasCap = true;
|
||||
}
|
||||
|
||||
if(shouldSpace) {
|
||||
buffer[bufferIndex++] = ' ';
|
||||
if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
|
||||
}
|
||||
if(shouldCap) {
|
||||
if(ch >= 'a' && ch <= 'z')
|
||||
ch -= 32;
|
||||
shouldCap = false;
|
||||
}
|
||||
buffer[bufferIndex++] = ch;
|
||||
}
|
||||
return buffer[0 .. bufferIndex].idup;
|
||||
}
|
||||
|
||||
/+
|
||||
Argument conversions: for the most part, it is to!Thing(string).
|
||||
|
||||
|
@ -5728,47 +5769,522 @@ private string urlify(string name) {
|
|||
}
|
||||
|
||||
foo&foo.a=1&foo.a=2&foo&foo.a=1
|
||||
|
||||
|
||||
Associative arrays are formatted with brackets, after a declaration, like structs:
|
||||
|
||||
foo&foo[key]=value&foo[other_key]=value
|
||||
|
||||
|
||||
Note: for maximum compatibility with outside code, keep your types simple. Some libraries
|
||||
do not support the strict ordering requirements to work with these struct protocols.
|
||||
|
||||
FIXME: also perhaps accept application/json to better work with outside trash.
|
||||
|
||||
|
||||
Return values are also auto-formatted according to user-requested type:
|
||||
for json, it loops over and converts.
|
||||
for html, basic types are strings. Arrays are <ol>. Structs are <dl>. Arrays of structs are tables!
|
||||
+/
|
||||
void callFromCgi(alias method, T)(T dg, Cgi cgi) {
|
||||
|
||||
// actually returns an arsd.dom.Form
|
||||
auto createAutomaticFormForFunction(alias method, T)(T dg) {
|
||||
import arsd.dom;
|
||||
|
||||
auto form = cast(Form) Element.make("form");
|
||||
|
||||
form.addClass("automatic-form");
|
||||
|
||||
form.addChild("h3", beautify(__traits(identifier, method)));
|
||||
|
||||
import std.traits;
|
||||
|
||||
Parameters!method params;
|
||||
static if(is(ReturnType!method == void)) {
|
||||
dg(params);
|
||||
} else {
|
||||
auto ret = dg(params);
|
||||
cgi.write(ret, true);
|
||||
//Parameters!method params;
|
||||
//alias idents = ParameterIdentifierTuple!method;
|
||||
//alias defaults = ParameterDefaults!method;
|
||||
|
||||
static Element elementFor(T)(string displayName, string name) {
|
||||
auto div = Element.make("div");
|
||||
div.addClass("form-field");
|
||||
|
||||
static if(is(T == struct)) {
|
||||
if(displayName !is null)
|
||||
div.addChild("span", displayName, "label-text");
|
||||
auto fieldset = div.addChild("fieldset");
|
||||
fieldset.addChild("legend", beautify(T.stringof)); // FIXME
|
||||
fieldset.addChild("input", name);
|
||||
static foreach(idx, memberName; __traits(allMembers, T))
|
||||
static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) {
|
||||
fieldset.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(beautify(memberName), name ~ "." ~ memberName));
|
||||
}
|
||||
} else static if(isSomeString!T || isIntegral!T || isFloatingPoint!T) {
|
||||
Element lbl;
|
||||
if(displayName !is null) {
|
||||
lbl = div.addChild("label");
|
||||
lbl.addChild("span", displayName, "label-text");
|
||||
lbl.appendText(" ");
|
||||
} else {
|
||||
lbl = div;
|
||||
}
|
||||
auto i = lbl.addChild("input", name);
|
||||
i.attrs.name = name;
|
||||
static if(isSomeString!T)
|
||||
i.attrs.type = "text";
|
||||
else
|
||||
i.attrs.type = "number";
|
||||
i.attrs.value = to!string(T.init);
|
||||
} else static if(is(T == K[], K)) {
|
||||
auto templ = div.addChild("template");
|
||||
templ.appendChild(elementFor!(K)(null, name));
|
||||
if(displayName !is null)
|
||||
div.addChild("span", displayName, "label-text");
|
||||
auto btn = div.addChild("button");
|
||||
btn.addClass("add-array-button");
|
||||
btn.attrs.type = "button";
|
||||
btn.innerText = "Add";
|
||||
btn.attrs.onclick = q{
|
||||
var a = document.importNode(this.parentNode.firstChild.content, true);
|
||||
this.parentNode.insertBefore(a, this);
|
||||
};
|
||||
} else static if(is(T == V[K], K, V)) {
|
||||
div.innerText = "assoc array not implemented for automatic form at this time";
|
||||
} else {
|
||||
static assert(0, "unsupported type for cgi call " ~ T.stringof);
|
||||
}
|
||||
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
static if(is(typeof(method) P == __parameters))
|
||||
static foreach(idx, _; P) {{
|
||||
alias param = P[idx .. idx + 1];
|
||||
string displayName = beautify(__traits(identifier, param));
|
||||
static foreach(attr; __traits(getAttributes, param))
|
||||
static if(is(typeof(attr) == DisplayName))
|
||||
displayName = attr.name;
|
||||
form.appendChild(elementFor!(param)(displayName, __traits(identifier, param)));
|
||||
}}
|
||||
|
||||
form.addChild("div", Html(`<input type="submit" value="Submit" />`), "submit-button-holder");
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
/*
|
||||
string urlFor(alias func)() {
|
||||
return __traits(identifier, func);
|
||||
}
|
||||
*/
|
||||
|
||||
/++
|
||||
UDA: The name displayed to the user in auto-generated HTML.
|
||||
|
||||
Default is `beautify(identifier)`.
|
||||
+/
|
||||
struct DisplayName {
|
||||
string name;
|
||||
}
|
||||
|
||||
/++
|
||||
UDA: The name used in the URL or web parameter.
|
||||
|
||||
Default is `urlify(identifier)` for functions and `identifier` for parameters and data members.
|
||||
+/
|
||||
struct UrlName {
|
||||
string name;
|
||||
}
|
||||
|
||||
class MissingArgumentException : Exception {
|
||||
string functionName;
|
||||
string argumentName;
|
||||
string argumentType;
|
||||
|
||||
this(string functionName, string argumentName, string argumentType, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
|
||||
this.functionName = functionName;
|
||||
this.argumentName = argumentName;
|
||||
this.argumentType = argumentType;
|
||||
|
||||
super("Missing Argument", file, line, next);
|
||||
}
|
||||
}
|
||||
|
||||
class WebObject {
|
||||
auto callFromCgi(alias method, T)(T dg, Cgi cgi) {
|
||||
|
||||
// FIXME: think more about checkboxes and bools.
|
||||
|
||||
import std.traits;
|
||||
|
||||
Parameters!method params;
|
||||
alias idents = ParameterIdentifierTuple!method;
|
||||
alias defaults = ParameterDefaults!method;
|
||||
|
||||
const(string)[] names;
|
||||
const(string)[] values;
|
||||
|
||||
// first, check for missing arguments and initialize to defaults if necessary
|
||||
static foreach(idx, param; params) {{
|
||||
auto ident = idents[idx];
|
||||
if(cgi.requestMethod == Cgi.RequestMethod.POST) {
|
||||
if(ident !in cgi.post) {
|
||||
static if(is(defaults[idx] == void))
|
||||
throw new MissingArgumentException(__traits(identifier, method), ident, typeof(param).stringof);
|
||||
else
|
||||
params[idx] = defaults[idx];
|
||||
}
|
||||
} else {
|
||||
if(ident !in cgi.get) {
|
||||
static if(is(defaults[idx] == void))
|
||||
throw new MissingArgumentException(__traits(identifier, method), ident, typeof(param).stringof);
|
||||
else
|
||||
params[idx] = defaults[idx];
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
// second, parse the arguments in order to build up arrays, etc.
|
||||
|
||||
static bool setVariable(T)(string name, string paramName, T* what, string value) {
|
||||
static if(is(T == struct)) {
|
||||
if(name == paramName) {
|
||||
*what = T.init;
|
||||
return true;
|
||||
} else {
|
||||
// could be a child
|
||||
if(name[paramName.length] == '.') {
|
||||
paramName = name[paramName.length + 1 .. $];
|
||||
name = paramName;
|
||||
int p = 0;
|
||||
foreach(ch; paramName) {
|
||||
if(ch == '.' || ch == '[')
|
||||
break;
|
||||
p++;
|
||||
}
|
||||
|
||||
// set the child member
|
||||
switch(paramName) {
|
||||
static foreach(idx, memberName; __traits(allMembers, T))
|
||||
static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) {
|
||||
// data member!
|
||||
case memberName:
|
||||
return setVariable(name, paramName, &(__traits(getMember, *what, memberName)), value);
|
||||
}
|
||||
default:
|
||||
// ok, not a member
|
||||
}
|
||||
}
|
||||
}
|
||||
} else static if(isSomeString!T || isIntegral!T || isFloatingPoint!T) {
|
||||
*what = to!T(value);
|
||||
return true;
|
||||
} else static if(is(T == K[], K)) {
|
||||
K tmp;
|
||||
if(name == paramName) {
|
||||
// direct - set and append
|
||||
if(setVariable(name, paramName, &tmp, value)) {
|
||||
(*what) ~= tmp;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// child, append to last element
|
||||
// FIXME: what about range violations???
|
||||
auto ptr = &(*what)[(*what).length - 1];
|
||||
return setVariable(name, paramName, ptr, value);
|
||||
|
||||
}
|
||||
} else static if(is(T == V[K], K, V)) {
|
||||
// assoc array, name[key] is valid
|
||||
if(name == paramName) {
|
||||
// no action necessary
|
||||
return true;
|
||||
} else if(name[paramName.length] == '[') {
|
||||
int count = 1;
|
||||
auto idx = paramName.length + 1;
|
||||
while(idx < name.length && count > 0) {
|
||||
if(name[idx] == '[')
|
||||
count++;
|
||||
else if(name[idx] == ']') {
|
||||
count--;
|
||||
if(count == 0) break;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
if(idx == name.length)
|
||||
return false; // malformed
|
||||
|
||||
auto insideBrackets = name[paramName.length + 1 .. idx];
|
||||
auto afterName = name[idx + 1 .. $];
|
||||
|
||||
auto k = to!K(insideBrackets);
|
||||
V v;
|
||||
|
||||
name = name[0 .. paramName.length];
|
||||
writeln(name, afterName, " ", paramName);
|
||||
|
||||
auto ret = setVariable(name ~ afterName, paramName, &v, value);
|
||||
if(ret) {
|
||||
(*what)[k] = v;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
static assert(0, "unsupported type for cgi call " ~ T.stringof);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void setArgument(string name, string value) {
|
||||
int p;
|
||||
foreach(ch; name) {
|
||||
if(ch == '.' || ch == '[')
|
||||
break;
|
||||
p++;
|
||||
}
|
||||
|
||||
auto paramName = name[0 .. p];
|
||||
|
||||
sw: switch(paramName) {
|
||||
static foreach(idx, param; params) {
|
||||
case idents[idx]:
|
||||
setVariable(name, paramName, ¶ms[idx], value);
|
||||
break sw;
|
||||
}
|
||||
default:
|
||||
// ignore; not relevant argument
|
||||
}
|
||||
}
|
||||
|
||||
if(cgi.requestMethod == Cgi.RequestMethod.POST) {
|
||||
names = cgi.allPostNamesInOrder;
|
||||
values = cgi.allPostValuesInOrder;
|
||||
} else {
|
||||
names = cgi.allGetNamesInOrder;
|
||||
values = cgi.allGetValuesInOrder;
|
||||
}
|
||||
|
||||
foreach(idx, name; names) {
|
||||
setArgument(name, values[idx]);
|
||||
}
|
||||
|
||||
static if(is(ReturnType!method == void)) {
|
||||
typeof(null) ret;
|
||||
dg(params);
|
||||
} else {
|
||||
auto ret = dg(params);
|
||||
}
|
||||
|
||||
// FIXME: format return values
|
||||
// options are: json, html, csv.
|
||||
// also may need to wrap in envelope format: none, html, or json.
|
||||
return ret;
|
||||
}
|
||||
|
||||
auto formatReturnValueAsHtml(T)(T t) {
|
||||
import arsd.dom;
|
||||
import std.traits;
|
||||
|
||||
static if(is(T == typeof(null))) {
|
||||
return Element.make("span");
|
||||
} else static if(isIntegral!T || isSomeString!T || isFloatingPoint!T) {
|
||||
return Element.make("span", to!string(t), "automatic-data-display");
|
||||
} else static if(is(T == V[K], K, V)) {
|
||||
auto dl = Element.make("dl");
|
||||
dl.addClass("automatic-data-display");
|
||||
foreach(k, v; t) {
|
||||
dl.addChild("dt", to!string(k));
|
||||
dl.addChild("dd", formatReturnValueAsHtml(v));
|
||||
}
|
||||
return dl;
|
||||
} else static if(is(T == struct)) {
|
||||
auto dl = Element.make("dl");
|
||||
dl.addClass("automatic-data-display");
|
||||
|
||||
static foreach(idx, memberName; __traits(allMembers, T))
|
||||
static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) {
|
||||
dl.addChild("dt", memberName);
|
||||
dl.addChild("dt", formatReturnValueAsHtml(__traits(getMember, t, memberName)));
|
||||
}
|
||||
|
||||
return dl;
|
||||
} else static if(is(T == E[], E)) {
|
||||
static if(is(E == struct)) {
|
||||
// an array of structs is kinda special in that I like
|
||||
// having those formatted as tables.
|
||||
auto table = cast(Table) Element.make("table");
|
||||
table.addClass("automatic-data-display");
|
||||
string[] names;
|
||||
static foreach(idx, memberName; __traits(allMembers, E))
|
||||
static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) {
|
||||
names ~= beautify(memberName);
|
||||
}
|
||||
table.appendHeaderRow(names);
|
||||
|
||||
foreach(l; t) {
|
||||
auto tr = table.appendRow();
|
||||
static foreach(idx, memberName; __traits(allMembers, E))
|
||||
static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) {
|
||||
tr.addChild("td", formatReturnValueAsHtml(__traits(getMember, l, memberName)));
|
||||
}
|
||||
}
|
||||
|
||||
return table;
|
||||
} else {
|
||||
// otherwise, I will just make a list.
|
||||
auto ol = Element.make("ol");
|
||||
ol.addClass("automatic-data-display");
|
||||
foreach(e; t)
|
||||
ol.addChild("li", formatReturnValueAsHtml(e));
|
||||
return ol;
|
||||
}
|
||||
} else static assert(0, "bad return value for cgi call " ~ T.stringof);
|
||||
|
||||
assert(0);
|
||||
}
|
||||
|
||||
/++
|
||||
The base class for the [dispatcher] object support.
|
||||
+/
|
||||
class WebObject() {
|
||||
Cgi cgi;
|
||||
void initialize(Cgi cgi) {
|
||||
this.cgi = cgi;
|
||||
}
|
||||
|
||||
string script() {
|
||||
return `
|
||||
`;
|
||||
}
|
||||
|
||||
string style() {
|
||||
return `
|
||||
:root {
|
||||
--mild-border: #ccc;
|
||||
--middle-border: #999;
|
||||
}
|
||||
table.automatic-data-display {
|
||||
border-collapse: collapse;
|
||||
border: solid 1px var(--mild-border);
|
||||
}
|
||||
|
||||
table.automatic-data-display td {
|
||||
vertical-align: top;
|
||||
border: solid 1px var(--mild-border);
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
table.automatic-data-display th {
|
||||
border: solid 1px var(--mild-border);
|
||||
border-bottom: solid 1px var(--middle-border);
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
ol.automatic-data-display {
|
||||
margin: 0px;
|
||||
list-style-position: inside;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.automatic-form {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
margin: 0.5em;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-left: -0.5em;
|
||||
}
|
||||
|
||||
.add-array-button {
|
||||
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
import arsd.dom;
|
||||
Element htmlContainer() {
|
||||
auto document = new Document(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>D Application</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>`, true, true);
|
||||
|
||||
return document.requireElementById("container");
|
||||
}
|
||||
}
|
||||
|
||||
/++
|
||||
Serves a class' methods. To be used with [dispatcher].
|
||||
Serves a class' methods, as a kind of low-state RPC over the web. To be used with [dispatcher].
|
||||
|
||||
Usage of this function will add a dependency on [arsd.dom] and [arsd.jsvar].
|
||||
|
||||
FIXME: explain this better
|
||||
+/
|
||||
auto serveApi(T)(string urlPrefix) {
|
||||
import arsd.dom;
|
||||
import arsd.jsvar;
|
||||
|
||||
static bool handler(string urlPrefix, Cgi cgi) {
|
||||
|
||||
auto obj = new T();
|
||||
obj.initialize(cgi);
|
||||
|
||||
switch(cgi.pathInfo[urlPrefix.length .. $]) {
|
||||
static foreach(methodName; __traits(derivedMembers, T))
|
||||
//static if(is({
|
||||
static foreach(methodName; __traits(derivedMembers, T)){{
|
||||
static if(is(typeof(__traits(getMember, T, methodName)) P == __parameters))
|
||||
{
|
||||
case urlify(methodName):
|
||||
callFromCgi!(__traits(getMember, obj, methodName))(&__traits(getMember, obj, methodName), cgi);
|
||||
switch(cgi.request("format", "html")) {
|
||||
case "html":
|
||||
auto container = obj.htmlContainer();
|
||||
try {
|
||||
auto ret = callFromCgi!(__traits(getMember, obj, methodName))(&__traits(getMember, obj, methodName), cgi);
|
||||
container.appendChild(formatReturnValueAsHtml(ret));
|
||||
} catch(MissingArgumentException mae) {
|
||||
container.appendChild(Element.make("p", "Argument `" ~ mae.argumentName ~ "` of type `" ~ mae.argumentType ~ "` is missing"));
|
||||
container.appendChild(createAutomaticFormForFunction!(__traits(getMember, obj, methodName))(&__traits(getMember, obj, methodName)));
|
||||
}
|
||||
cgi.write(container.parentDocument.toString(), true);
|
||||
break;
|
||||
case "json":
|
||||
auto ret = callFromCgi!(__traits(getMember, obj, methodName))(&__traits(getMember, obj, methodName), cgi);
|
||||
var json = ret;
|
||||
var envelope = var.emptyObject;
|
||||
envelope.success = true;
|
||||
envelope.result = json;
|
||||
envelope.error = null;
|
||||
cgi.setResponseContentType("application/json");
|
||||
cgi.write(envelope.toJson(), true);
|
||||
|
||||
break;
|
||||
default:
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}}
|
||||
case "script.js":
|
||||
cgi.setResponseContentType("text/javascript");
|
||||
cgi.gzipResponse = true;
|
||||
cgi.write(obj.script(), true);
|
||||
return true;
|
||||
case "style.css":
|
||||
cgi.setResponseContentType("text/css");
|
||||
cgi.gzipResponse = true;
|
||||
cgi.write(obj.style(), true);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
@ -5779,9 +6295,127 @@ auto serveApi(T)(string urlPrefix) {
|
|||
}
|
||||
|
||||
/++
|
||||
Serves a REST object.
|
||||
Serves a REST object, similar to a Ruby on Rails resource.
|
||||
|
||||
You put data members in your class. cgi.d will automatically make something out of those.
|
||||
|
||||
It will call your constructor with the ID from the URL. This may be null.
|
||||
It will then populate the data members from the request.
|
||||
It will then call a method, if present, telling what happened. You don't need to write these!
|
||||
It finally returns a reply.
|
||||
|
||||
Your methods are passed a list of fields it actually set.
|
||||
|
||||
The URL mapping - despite my general skepticism of the wisdom - matches up with what most REST
|
||||
APIs I have used seem to follow. (I REALLY want to put trailing slashes on it though. Works better
|
||||
with relative linking. But meh.)
|
||||
|
||||
GET /items -> index. all values not set.
|
||||
GET /items/id -> get. only ID will be set, other params ignored.
|
||||
POST /items -> create. values set as given
|
||||
PUT /items/id -> replace. values set as given
|
||||
or POST /items/id with cgi.post["_method"] (thus urlencoded or multipart content-type) set to "PUT" to work around browser/html limitation
|
||||
a GET with cgi.get["_method"] (in the url) set to "PUT" will render a form.
|
||||
PATCH /items/id -> update. values set as given, list of changed fields passed
|
||||
or POST /items/id with cgi.post["_method"] == "PATCH"
|
||||
DELETE /items/id -> destroy. only ID guaranteed to be set
|
||||
or POST /items/id with cgi.post["_method"] == "DELETE"
|
||||
|
||||
Following the stupid convention, there will never be a trailing slash here, and if it is there, it will
|
||||
redirect you away from it.
|
||||
|
||||
API clients should set the `Accept` HTTP header to application/json or the cgi.get["_format"] = "json" var.
|
||||
|
||||
I will also let you change the default, if you must.
|
||||
|
||||
// One add-on is validation. You can issue a HTTP GET to a resource with _method = VALIDATE to check potential changes.
|
||||
|
||||
You can define sub-resources on your object inside the object. These sub-resources are also REST objects
|
||||
that follow the same thing. They may be individual resources or collections themselves.
|
||||
|
||||
Your class is expected to have at least the following methods:
|
||||
|
||||
FIXME: i kinda wanna add a routes object to the initialize call
|
||||
|
||||
create
|
||||
Create returns the new address on success, some code on failure.
|
||||
show
|
||||
index
|
||||
update
|
||||
remove
|
||||
|
||||
You will want to be able to customize the HTTP, HTML, and JSON returns but generally shouldn't have to - the defaults
|
||||
should usually work. The returned JSON will include a field "href" on all returned objects along with "id". Or omething like that.
|
||||
|
||||
Usage of this function will add a dependency on [arsd.dom] and [arsd.jsvar].
|
||||
|
||||
NOT IMPLEMENTED
|
||||
|
||||
|
||||
Really, a collection is a resource with a bunch of subresources.
|
||||
|
||||
GET /items
|
||||
index because it is GET on the top resource
|
||||
|
||||
GET /items/foo
|
||||
item but different than items?
|
||||
|
||||
class Items {
|
||||
|
||||
}
|
||||
|
||||
... but meh, a collection can be automated. not worth making it
|
||||
a separate thing, let's look at a real example. Users has many
|
||||
items and a virtual one, /users/current.
|
||||
|
||||
the individual users have properties and two sub-resources:
|
||||
session, which is just one, and comments, a collection.
|
||||
|
||||
class User : RestObject!() { // no parent
|
||||
int id;
|
||||
string name;
|
||||
|
||||
void show() {} // automated! GET of this specific thing
|
||||
void create() {} // POST on a parent collection - this is called from a collection class after the members are updated
|
||||
void replace() {} // this is the PUT; really, it just updates all fields.
|
||||
void update() {} // PATCH, it updates some fields.
|
||||
void remove() {} // DELETE
|
||||
|
||||
void load(string urlId) {} // the default implementation of show() populates the id, then
|
||||
|
||||
this() {}
|
||||
|
||||
mixin Subresource!Session;
|
||||
mixin Subresource!Comment;
|
||||
}
|
||||
|
||||
class Session : RestObject!() {
|
||||
// the parent object may not be fully constructed/loaded
|
||||
this(User parent) {}
|
||||
|
||||
}
|
||||
|
||||
class Comment : CollectionOf!Comment {
|
||||
this(User parent) {}
|
||||
}
|
||||
|
||||
class Users : CollectionOf!User {
|
||||
// but you don't strictly need ANYTHING on a collection; it will just... collect. Implement the subobjects.
|
||||
void index() {} // GET on this specific thing; just like show really, just different name for the different semantics.
|
||||
User create() {} // You MAY implement this, but the default is to create a new object, populate it from args, and then call create() on the child
|
||||
}
|
||||
|
||||
// so CollectionOf will mixin the stuff to forward it
|
||||
|
||||
OK, the underlying functions are actually really low level
|
||||
|
||||
GET(string url)
|
||||
POST(string url)
|
||||
PUT(string url)
|
||||
|
||||
The url starts with the initial thing passed.
|
||||
|
||||
It is the mixins that actually do the work.
|
||||
+/
|
||||
auto serveRestObject(T)(string urlPrefix) {
|
||||
static bool handler(string urlPrefix, Cgi cgi) {
|
||||
|
|
Loading…
Reference in New Issue