mirror of https://github.com/adamdruppe/arsd.git
for now rest sample
This commit is contained in:
parent
3f15f41ddc
commit
076d28dba1
626
cgi.d
626
cgi.d
|
@ -1,6 +1,9 @@
|
||||||
// FIXME: if an exception is thrown, we shouldn't necessarily cache...
|
// FIXME: if an exception is thrown, we shouldn't necessarily cache...
|
||||||
// FIXME: there's some annoying duplication of code in the various versioned mains
|
// FIXME: there's some annoying duplication of code in the various versioned mains
|
||||||
|
|
||||||
|
// FIXME: I might make a cgi proxy class which can change things; the underlying one is still immutable
|
||||||
|
// but the later one can edit and simplify the api. You'd have to use the subclass tho!
|
||||||
|
|
||||||
/*
|
/*
|
||||||
void foo(int f, @("test") string s) {}
|
void foo(int f, @("test") string s) {}
|
||||||
|
|
||||||
|
@ -3031,9 +3034,14 @@ void cgiMainImpl(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxC
|
||||||
uint i;
|
uint i;
|
||||||
sockaddr addr;
|
sockaddr addr;
|
||||||
i = addr.sizeof;
|
i = addr.sizeof;
|
||||||
version(embedded_httpd_processes_accept_after_fork)
|
version(embedded_httpd_processes_accept_after_fork) {
|
||||||
int s = accept(sock, &addr, &i);
|
int s = accept(sock, &addr, &i);
|
||||||
else {
|
int opt = 1;
|
||||||
|
import core.sys.posix.netinet.tcp;
|
||||||
|
// the Cgi class does internal buffering, so disabling this
|
||||||
|
// helps with latency in many cases...
|
||||||
|
setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &opt, opt.sizeof);
|
||||||
|
} else {
|
||||||
int s;
|
int s;
|
||||||
auto readret = read_fd(pipeReadFd, &s, s.sizeof, &s);
|
auto readret = read_fd(pipeReadFd, &s, s.sizeof, &s);
|
||||||
if(readret != s.sizeof) {
|
if(readret != s.sizeof) {
|
||||||
|
@ -3147,6 +3155,9 @@ void cgiMainImpl(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxC
|
||||||
sockaddr addr;
|
sockaddr addr;
|
||||||
i = addr.sizeof;
|
i = addr.sizeof;
|
||||||
s = accept(sock, &addr, &i);
|
s = accept(sock, &addr, &i);
|
||||||
|
import core.sys.posix.netinet.tcp;
|
||||||
|
int opt = 1;
|
||||||
|
setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &opt, opt.sizeof);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(FD_ISSET(pipeWriteFd, &write_fds)) {
|
if(FD_ISSET(pipeWriteFd, &write_fds)) {
|
||||||
|
@ -5787,6 +5798,63 @@ private string beautify(string name) {
|
||||||
for html, basic types are strings. Arrays are <ol>. Structs are <dl>. Arrays of structs are tables!
|
for html, basic types are strings. Arrays are <ol>. Structs are <dl>. Arrays of structs are tables!
|
||||||
+/
|
+/
|
||||||
|
|
||||||
|
// returns an arsd.dom.Element
|
||||||
|
static auto elementFor(T)(string displayName, string name) {
|
||||||
|
import arsd.dom;
|
||||||
|
import std.traits;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// actually returns an arsd.dom.Form
|
// actually returns an arsd.dom.Form
|
||||||
auto createAutomaticFormForFunction(alias method, T)(T dg) {
|
auto createAutomaticFormForFunction(alias method, T)(T dg) {
|
||||||
import arsd.dom;
|
import arsd.dom;
|
||||||
|
@ -5803,59 +5871,6 @@ auto createAutomaticFormForFunction(alias method, T)(T dg) {
|
||||||
//alias idents = ParameterIdentifierTuple!method;
|
//alias idents = ParameterIdentifierTuple!method;
|
||||||
//alias defaults = ParameterDefaults!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 if(is(typeof(method) P == __parameters))
|
||||||
static foreach(idx, _; P) {{
|
static foreach(idx, _; P) {{
|
||||||
alias param = P[idx .. idx + 1];
|
alias param = P[idx .. idx + 1];
|
||||||
|
@ -5871,6 +5886,38 @@ auto createAutomaticFormForFunction(alias method, T)(T dg) {
|
||||||
return form;
|
return form;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// actually returns an arsd.dom.Form
|
||||||
|
auto createAutomaticFormForObject(T)(T obj) {
|
||||||
|
import arsd.dom;
|
||||||
|
|
||||||
|
auto form = cast(Form) Element.make("form");
|
||||||
|
|
||||||
|
form.addClass("automatic-form");
|
||||||
|
|
||||||
|
form.addChild("h3", beautify(__traits(identifier, T)));
|
||||||
|
|
||||||
|
import std.traits;
|
||||||
|
|
||||||
|
//Parameters!method params;
|
||||||
|
//alias idents = ParameterIdentifierTuple!method;
|
||||||
|
//alias defaults = ParameterDefaults!method;
|
||||||
|
|
||||||
|
static foreach(idx, memberName; __traits(derivedMembers, T)) {{
|
||||||
|
static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {
|
||||||
|
string displayName = beautify(memberName);
|
||||||
|
static foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName)))
|
||||||
|
static if(is(typeof(attr) == DisplayName))
|
||||||
|
displayName = attr.name;
|
||||||
|
form.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(displayName, memberName));
|
||||||
|
|
||||||
|
form.setValue(memberName, to!string(__traits(getMember, obj, memberName)));
|
||||||
|
}}}
|
||||||
|
|
||||||
|
form.addChild("div", Html(`<input type="submit" value="Submit" />`), "submit-button-holder");
|
||||||
|
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
string urlFor(alias func)() {
|
string urlFor(alias func)() {
|
||||||
return __traits(identifier, func);
|
return __traits(identifier, func);
|
||||||
|
@ -5911,6 +5958,8 @@ class MissingArgumentException : Exception {
|
||||||
|
|
||||||
auto callFromCgi(alias method, T)(T dg, Cgi cgi) {
|
auto callFromCgi(alias method, T)(T dg, Cgi cgi) {
|
||||||
|
|
||||||
|
// FIXME: any array of structs should also be settable or gettable from csv as well.
|
||||||
|
|
||||||
// FIXME: think more about checkboxes and bools.
|
// FIXME: think more about checkboxes and bools.
|
||||||
|
|
||||||
import std.traits;
|
import std.traits;
|
||||||
|
@ -6110,7 +6159,32 @@ auto formatReturnValueAsHtml(T)(T t) {
|
||||||
|
|
||||||
return dl;
|
return dl;
|
||||||
} else static if(is(T == E[], E)) {
|
} else static if(is(T == E[], E)) {
|
||||||
static if(is(E == struct)) {
|
static if(is(E : RestObject!Proxy, Proxy)) {
|
||||||
|
// treat RestObject similar to struct
|
||||||
|
auto table = cast(Table) Element.make("table");
|
||||||
|
table.addClass("automatic-data-display");
|
||||||
|
string[] names;
|
||||||
|
static foreach(idx, memberName; __traits(derivedMembers, 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(derivedMembers, E))
|
||||||
|
static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) {
|
||||||
|
static if(memberName == "id") {
|
||||||
|
string val = to!string(__traits(getMember, l, memberName));
|
||||||
|
tr.addChild("td", Element.make("a", val, E.stringof.toLower ~ "s/" ~ val)); // FIXME
|
||||||
|
} else {
|
||||||
|
tr.addChild("td", formatReturnValueAsHtml(__traits(getMember, l, memberName)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return table;
|
||||||
|
} else static if(is(E == struct)) {
|
||||||
// an array of structs is kinda special in that I like
|
// an array of structs is kinda special in that I like
|
||||||
// having those formatted as tables.
|
// having those formatted as tables.
|
||||||
auto table = cast(Table) Element.make("table");
|
auto table = cast(Table) Element.make("table");
|
||||||
|
@ -6145,7 +6219,19 @@ auto formatReturnValueAsHtml(T)(T t) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/++
|
/++
|
||||||
The base class for the [dispatcher] object support.
|
A web presenter is responsible for rendering things to HTML to be usable
|
||||||
|
in a web browser.
|
||||||
|
|
||||||
|
They are passed as template arguments to the base classes of [WebObject]
|
||||||
|
|
||||||
|
FIXME
|
||||||
|
+/
|
||||||
|
class WebPresenter() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
The base class for the [dispatcher] function and object support.
|
||||||
+/
|
+/
|
||||||
class WebObject(Helper = void) {
|
class WebObject(Helper = void) {
|
||||||
Cgi cgi;
|
Cgi cgi;
|
||||||
|
@ -6163,7 +6249,14 @@ class WebObject(Helper = void) {
|
||||||
:root {
|
:root {
|
||||||
--mild-border: #ccc;
|
--mild-border: #ccc;
|
||||||
--middle-border: #999;
|
--middle-border: #999;
|
||||||
|
--accent-color: #e8e8e8;
|
||||||
|
--sidebar-color: #f2f2f2;
|
||||||
}
|
}
|
||||||
|
` ~ genericFormStyling() ~ genericSiteStyling();
|
||||||
|
}
|
||||||
|
|
||||||
|
string genericFormStyling() {
|
||||||
|
return `
|
||||||
table.automatic-data-display {
|
table.automatic-data-display {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
border: solid 1px var(--mild-border);
|
border: solid 1px var(--mild-border);
|
||||||
|
@ -6208,6 +6301,39 @@ class WebObject(Helper = void) {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string genericSiteStyling() {
|
||||||
|
return `
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0px; }
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
#header {
|
||||||
|
background: var(--accent-color);
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
#footer {
|
||||||
|
background: var(--accent-color);
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
#main-site {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
#container {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
order: 2;
|
||||||
|
min-height: calc(100vh - 64px - 64px);
|
||||||
|
padding: 4px;
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
#sidebar {
|
||||||
|
flex: 0 0 16em;
|
||||||
|
order: 1;
|
||||||
|
background: var(--sidebar-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
import arsd.dom;
|
import arsd.dom;
|
||||||
Element htmlContainer() {
|
Element htmlContainer() {
|
||||||
auto document = new Document(`<!DOCTYPE html>
|
auto document = new Document(`<!DOCTYPE html>
|
||||||
|
@ -6217,7 +6343,12 @@ class WebObject(Helper = void) {
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="style.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="container"></div>
|
<div id="header"></div>
|
||||||
|
<div id="main-site">
|
||||||
|
<div id="container"></div>
|
||||||
|
<div id="sidebar"></div>
|
||||||
|
</div>
|
||||||
|
<div id="footer"></div>
|
||||||
<script src="script.js"></script>
|
<script src="script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>`, true, true);
|
</html>`, true, true);
|
||||||
|
@ -6296,21 +6427,6 @@ auto serveApi(T)(string urlPrefix) {
|
||||||
return DispatcherDefinition!handler(urlPrefix, false);
|
return DispatcherDefinition!handler(urlPrefix, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/++
|
|
||||||
The base of all REST objects.
|
|
||||||
+/
|
|
||||||
class RestObject(Helper = void) {
|
|
||||||
|
|
||||||
import arsd.dom;
|
|
||||||
import arsd.jsvar;
|
|
||||||
|
|
||||||
/// Show
|
|
||||||
void show() {}
|
|
||||||
/// ditto
|
|
||||||
void show(string urlId) {
|
|
||||||
load(urlId);
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AccessCheck {
|
enum AccessCheck {
|
||||||
allowed,
|
allowed,
|
||||||
|
@ -6339,6 +6455,23 @@ class RestObject(Helper = void) {
|
||||||
invalid
|
invalid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/++
|
||||||
|
The base of all REST objects, to be used with [serveRestObject] and [serveRestCollectionOf].
|
||||||
|
+/
|
||||||
|
class RestObject(Helper = void) : WebObject!Helper {
|
||||||
|
|
||||||
|
import arsd.dom;
|
||||||
|
import arsd.jsvar;
|
||||||
|
|
||||||
|
/// Prepare the object to be shown.
|
||||||
|
void show() {}
|
||||||
|
/// ditto
|
||||||
|
void show(string urlId) {
|
||||||
|
load(urlId);
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
|
||||||
ValidationResult delegate(typeof(this)) validateFromReflection;
|
ValidationResult delegate(typeof(this)) validateFromReflection;
|
||||||
Element delegate(typeof(this)) toHtmlFromReflection;
|
Element delegate(typeof(this)) toHtmlFromReflection;
|
||||||
var delegate(typeof(this)) toJsonFromReflection;
|
var delegate(typeof(this)) toJsonFromReflection;
|
||||||
|
@ -6357,7 +6490,14 @@ class RestObject(Helper = void) {
|
||||||
// The functions with more arguments are the low-level ones,
|
// The functions with more arguments are the low-level ones,
|
||||||
// they forward to the ones with fewer arguments by default.
|
// they forward to the ones with fewer arguments by default.
|
||||||
|
|
||||||
void create() {} // POST on a parent collection - this is called from a collection class after the members are updated
|
// POST on a parent collection - this is called from a collection class after the members are updated
|
||||||
|
/++
|
||||||
|
Given a populated object, this creates a new entry. Returns the url identifier
|
||||||
|
of the new object.
|
||||||
|
+/
|
||||||
|
string create(scope void delegate() applyChanges) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
void replace() {}
|
void replace() {}
|
||||||
void replace(string urlId, scope void delegate() applyChanges) {
|
void replace(string urlId, scope void delegate() applyChanges) {
|
||||||
|
@ -6380,8 +6520,8 @@ class RestObject(Helper = void) {
|
||||||
remove();
|
remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract void load(string urlId) {}
|
abstract void load(string urlId);
|
||||||
abstract void save() {}
|
abstract void save();
|
||||||
|
|
||||||
Element toHtml() {
|
Element toHtml() {
|
||||||
if(toHtmlFromReflection)
|
if(toHtmlFromReflection)
|
||||||
|
@ -6396,14 +6536,55 @@ class RestObject(Helper = void) {
|
||||||
else
|
else
|
||||||
assert(0);
|
assert(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/+
|
||||||
|
auto structOf(this This) {
|
||||||
|
|
||||||
|
}
|
||||||
|
+/
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Responsible for displaying stuff as HTML. You can put this into your own aggregate
|
||||||
|
and override it. Use forwarding and specialization to customize it.
|
||||||
|
+/
|
||||||
|
mixin template Presenter() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/++
|
/++
|
||||||
Base class for REST collections.
|
Base class for REST collections.
|
||||||
+/
|
+/
|
||||||
class CollectionOf(Obj, Helper = void) : RestObject!(Helper) {
|
class CollectionOf(Obj, Helper = void) : RestObject!(Helper) {
|
||||||
void index() {}
|
/// You might subclass this and use the cgi object's query params
|
||||||
override void create() {}
|
/// to implement a search filter, for example.
|
||||||
|
///
|
||||||
|
/// FIXME: design a way to auto-generate that form
|
||||||
|
/// (other than using the WebObject thing above lol
|
||||||
|
// it'll prolly just be some searchParams UDA or maybe an enum.
|
||||||
|
//
|
||||||
|
// pagination too perhaps.
|
||||||
|
//
|
||||||
|
// and sorting too
|
||||||
|
IndexResult index() { return IndexResult.init; }
|
||||||
|
|
||||||
|
string[] sortableFields() { return null; }
|
||||||
|
string[] searchableFields() { return null; }
|
||||||
|
|
||||||
|
struct IndexResult {
|
||||||
|
Obj[] results;
|
||||||
|
|
||||||
|
string[] sortableFields;
|
||||||
|
|
||||||
|
string previousPageIdentifier;
|
||||||
|
string nextPageIdentifier;
|
||||||
|
string firstPageIdentifier;
|
||||||
|
string lastPageIdentifier;
|
||||||
|
|
||||||
|
int numberOfPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
override string create(scope void delegate() applyChanges) { assert(0); }
|
||||||
override void load(string urlId) { assert(0); }
|
override void load(string urlId) { assert(0); }
|
||||||
override void save() { assert(0); }
|
override void save() { assert(0); }
|
||||||
override void show() {
|
override void show() {
|
||||||
|
@ -6412,6 +6593,9 @@ class CollectionOf(Obj, Helper = void) : RestObject!(Helper) {
|
||||||
override void show(string urlId) {
|
override void show(string urlId) {
|
||||||
show();
|
show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Proxy POST requests (create calls) to the child collection
|
||||||
|
alias PostProxy = Obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
/++
|
/++
|
||||||
|
@ -6539,49 +6723,265 @@ auto serveRestObject(T)(string urlPrefix) {
|
||||||
|
|
||||||
if(url.length && url[$ - 1] == '/') {
|
if(url.length && url[$ - 1] == '/') {
|
||||||
// remove the final slash...
|
// remove the final slash...
|
||||||
cgi.setResponseLocation("..");
|
cgi.setResponseLocation(cgi.pathInfo[0 .. $ - 1]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
string urlId = null;
|
return restObjectServeHandler!T(cgi, url);
|
||||||
if(url.length && url[0] == '/') {
|
|
||||||
// asking for a subobject
|
|
||||||
urlId = url[1 .. $];
|
|
||||||
foreach(idx, ch; urlId) {
|
|
||||||
if(ch == '/') {
|
|
||||||
urlId = urlId[0 .. idx];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: support precondition failed, if-modified-since, expectation failed, etc.
|
|
||||||
|
|
||||||
auto obj = new T();
|
|
||||||
obj.toHtmlFromReflection = delegate(t) {
|
|
||||||
import arsd.dom;
|
|
||||||
return Element.make("div", T.stringof ~ "/" ~ urlId);
|
|
||||||
};
|
|
||||||
// FIXME: populate reflection info delegates
|
|
||||||
|
|
||||||
switch(cgi.requestMethod) {
|
|
||||||
case Cgi.RequestMethod.GET:
|
|
||||||
obj.show(urlId);
|
|
||||||
cgi.write(obj.toHtml().toString, true);
|
|
||||||
break;
|
|
||||||
case Cgi.RequestMethod.POST:
|
|
||||||
case Cgi.RequestMethod.PUT:
|
|
||||||
case Cgi.RequestMethod.PATCH:
|
|
||||||
case Cgi.RequestMethod.DELETE:
|
|
||||||
default:
|
|
||||||
// FIXME: OPTIONS, HEAD
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return DispatcherDefinition!handler(urlPrefix, false);
|
return DispatcherDefinition!handler(urlPrefix, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convenience method for serving a collection. It will be named the same
|
||||||
|
/// as type T, just with an s at the end. If you need any further, just
|
||||||
|
/// write the class yourself.
|
||||||
|
auto serveRestCollectionOf(T)(string urlPrefix) {
|
||||||
|
mixin(`static class `~T.stringof~`s : CollectionOf!(T) {}`);
|
||||||
|
return serveRestObject!(mixin(T.stringof ~ "s"))(urlPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool restObjectServeHandler(T)(Cgi cgi, string url) {
|
||||||
|
string urlId = null;
|
||||||
|
if(url.length && url[0] == '/') {
|
||||||
|
// asking for a subobject
|
||||||
|
urlId = url[1 .. $];
|
||||||
|
foreach(idx, ch; urlId) {
|
||||||
|
if(ch == '/') {
|
||||||
|
urlId = urlId[0 .. idx];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME handle other subresources
|
||||||
|
|
||||||
|
static if(is(T : CollectionOf!(C, P), C, P)) {
|
||||||
|
if(urlId !is null) {
|
||||||
|
return restObjectServeHandler!C(cgi, url); // FIXME? urlId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: support precondition failed, if-modified-since, expectation failed, etc.
|
||||||
|
|
||||||
|
auto obj = new T();
|
||||||
|
obj.toHtmlFromReflection = delegate(t) {
|
||||||
|
import arsd.dom;
|
||||||
|
auto div = Element.make("div");
|
||||||
|
div.addClass("Dclass_" ~ T.stringof);
|
||||||
|
div.dataset.url = urlId;
|
||||||
|
static foreach(idx, memberName; __traits(derivedMembers, T))
|
||||||
|
static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {
|
||||||
|
div.appendChild(formatReturnValueAsHtml(__traits(getMember, obj, memberName)));
|
||||||
|
}
|
||||||
|
return div;
|
||||||
|
};
|
||||||
|
obj.toJsonFromReflection = delegate(t) {
|
||||||
|
import arsd.jsvar;
|
||||||
|
var v = var.emptyObject();
|
||||||
|
static foreach(idx, memberName; __traits(derivedMembers, T))
|
||||||
|
static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {
|
||||||
|
v[memberName] = __traits(getMember, obj, memberName);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
};
|
||||||
|
obj.validateFromReflection = delegate(t) {
|
||||||
|
// FIXME
|
||||||
|
return ValidationResult.valid;
|
||||||
|
};
|
||||||
|
obj.initialize(cgi);
|
||||||
|
// FIXME: populate reflection info delegates
|
||||||
|
|
||||||
|
|
||||||
|
// FIXME: I am not happy with this.
|
||||||
|
switch(urlId) {
|
||||||
|
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:
|
||||||
|
// intentionally blank
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
static void applyChangesTemplate(Obj)(Cgi cgi, Obj obj) {
|
||||||
|
static foreach(idx, memberName; __traits(derivedMembers, Obj))
|
||||||
|
static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {
|
||||||
|
__traits(getMember, obj, memberName) = cgi.request(memberName, __traits(getMember, obj, memberName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void applyChanges() {
|
||||||
|
applyChangesTemplate(cgi, obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] modifiedList;
|
||||||
|
|
||||||
|
void writeObject(bool addFormLinks) {
|
||||||
|
if(cgi.request("format") == "json") {
|
||||||
|
cgi.setResponseContentType("application/json");
|
||||||
|
cgi.write(obj.toJson().toString, true);
|
||||||
|
} else {
|
||||||
|
auto container = obj.htmlContainer();
|
||||||
|
if(addFormLinks) {
|
||||||
|
static if(is(T : CollectionOf!(C, P), C, P))
|
||||||
|
container.appendHtml(`
|
||||||
|
<form>
|
||||||
|
<button type="submit" name="_method" value="POST">Create New</button>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
else
|
||||||
|
container.appendHtml(`
|
||||||
|
<form>
|
||||||
|
<button type="submit" name="_method" value="PATCH">Edit</button>
|
||||||
|
<button type="submit" name="_method" value="DELETE">Delete</button>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
container.appendChild(obj.toHtml());
|
||||||
|
cgi.write(container.parentDocument.toString, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: I think I need a set type in here....
|
||||||
|
// it will be nice to pass sets of members.
|
||||||
|
|
||||||
|
switch(cgi.requestMethod) {
|
||||||
|
case Cgi.RequestMethod.GET:
|
||||||
|
// I could prolly use template this parameters in the implementation above for some reflection stuff.
|
||||||
|
// sure, it doesn't automatically work in subclasses... but I instantiate here anyway...
|
||||||
|
|
||||||
|
// automatic forms here for usable basic auto site from browser.
|
||||||
|
// even if the format is json, it could actually send out the links and formats, but really there i'ma be meh.
|
||||||
|
switch(cgi.request("_method", "GET")) {
|
||||||
|
case "GET":
|
||||||
|
static if(is(T : CollectionOf!(C, P), C, P)) {
|
||||||
|
auto results = obj.index();
|
||||||
|
if(cgi.request("format", "html") == "html") {
|
||||||
|
auto container = obj.htmlContainer();
|
||||||
|
auto html = formatReturnValueAsHtml(results.results);
|
||||||
|
container.appendHtml(`
|
||||||
|
<form>
|
||||||
|
<button type="submit" name="_method" value="POST">Create New</button>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
|
||||||
|
container.appendChild(html);
|
||||||
|
cgi.write(container.parentDocument.toString, true);
|
||||||
|
} else {
|
||||||
|
cgi.setResponseContentType("application/json");
|
||||||
|
import arsd.jsvar;
|
||||||
|
var json = var.emptyArray;
|
||||||
|
foreach(r; results.results) {
|
||||||
|
var o = var.emptyObject;
|
||||||
|
static foreach(idx, memberName; __traits(derivedMembers, typeof(r)))
|
||||||
|
static if(__traits(compiles, __traits(getMember, r, memberName).offsetof)) {
|
||||||
|
o[memberName] = __traits(getMember, r, memberName);
|
||||||
|
}
|
||||||
|
|
||||||
|
json ~= o;
|
||||||
|
}
|
||||||
|
cgi.write(json.toJson(), true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obj.show(urlId);
|
||||||
|
writeObject(true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "PATCH":
|
||||||
|
obj.load(urlId);
|
||||||
|
goto case;
|
||||||
|
case "PUT":
|
||||||
|
case "POST":
|
||||||
|
// an editing form for the object
|
||||||
|
auto container = obj.htmlContainer();
|
||||||
|
static if(__traits(compiles, () { auto o = new obj.PostProxy(); })) {
|
||||||
|
auto form = (cgi.request("_method") == "POST") ? createAutomaticFormForObject(new obj.PostProxy()) : createAutomaticFormForObject(obj);
|
||||||
|
} else {
|
||||||
|
auto form = createAutomaticFormForObject(obj);
|
||||||
|
}
|
||||||
|
form.attrs.method = "POST";
|
||||||
|
form.setValue("_method", cgi.request("_method", "GET"));
|
||||||
|
container.appendChild(form);
|
||||||
|
cgi.write(container.parentDocument.toString(), true);
|
||||||
|
break;
|
||||||
|
case "DELETE":
|
||||||
|
// FIXME: a delete form for the object (can be phrased "are you sure?")
|
||||||
|
auto container = obj.htmlContainer();
|
||||||
|
container.appendHtml(`
|
||||||
|
<form method="POST">
|
||||||
|
Are you sure you want to delete this item?
|
||||||
|
<input type="hidden" name="_method" value="DELETE" />
|
||||||
|
<input type="submit" value="Yes, Delete It" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
`);
|
||||||
|
cgi.write(container.parentDocument.toString(), true);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
cgi.write("bad method\n", true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Cgi.RequestMethod.POST:
|
||||||
|
// this is to allow compatibility with HTML forms
|
||||||
|
switch(cgi.request("_method", "POST")) {
|
||||||
|
case "PUT":
|
||||||
|
goto PUT;
|
||||||
|
case "PATCH":
|
||||||
|
goto PATCH;
|
||||||
|
case "DELETE":
|
||||||
|
goto DELETE;
|
||||||
|
case "POST":
|
||||||
|
static if(__traits(compiles, () { auto o = new obj.PostProxy(); })) {
|
||||||
|
auto p = new obj.PostProxy();
|
||||||
|
void specialApplyChanges() {
|
||||||
|
applyChangesTemplate(cgi, p);
|
||||||
|
}
|
||||||
|
string n = p.create(&specialApplyChanges);
|
||||||
|
} else {
|
||||||
|
string n = obj.create(&applyChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto newUrl = cgi.pathInfo ~ "/" ~ n;
|
||||||
|
cgi.setResponseLocation(newUrl);
|
||||||
|
cgi.setResponseStatus("201 Created");
|
||||||
|
cgi.write(`The object has been created.`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
cgi.write("bad method\n", true);
|
||||||
|
}
|
||||||
|
// FIXME this should be valid on the collection, but not the child....
|
||||||
|
// 303 See Other
|
||||||
|
break;
|
||||||
|
case Cgi.RequestMethod.PUT:
|
||||||
|
PUT:
|
||||||
|
obj.replace(urlId, &applyChanges);
|
||||||
|
writeObject(false);
|
||||||
|
break;
|
||||||
|
case Cgi.RequestMethod.PATCH:
|
||||||
|
PATCH:
|
||||||
|
obj.update(urlId, &applyChanges, modifiedList);
|
||||||
|
writeObject(false);
|
||||||
|
break;
|
||||||
|
case Cgi.RequestMethod.DELETE:
|
||||||
|
DELETE:
|
||||||
|
obj.remove(urlId);
|
||||||
|
cgi.setResponseStatus("204 No Content");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// FIXME: OPTIONS, HEAD
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/++
|
/++
|
||||||
Serves a static file. To be used with [dispatcher].
|
Serves a static file. To be used with [dispatcher].
|
||||||
+/
|
+/
|
||||||
|
|
Loading…
Reference in New Issue