more dispatchermagic

This commit is contained in:
Adam D. Ruppe 2021-12-22 18:47:51 -05:00
parent 933423478f
commit 30586da8bf
1 changed files with 144 additions and 11 deletions

155
cgi.d
View File

@ -8518,6 +8518,25 @@ class MissingArgumentException : Exception {
} }
} }
/++
You can throw this from an api handler to indicate a 404 response. This is done by the presentExceptionAsHtml function in the presenter.
History:
Added December 15, 2021 (dub v10.5)
+/
class ResourceNotFoundException : Exception {
string resourceType;
string resourceId;
this(string resourceType, string resourceId, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
this.resourceType = resourceType;
this.resourceId = resourceId;
super("Resource not found: " ~ resourceType ~ " " ~ resourceId, file, line, next);
}
}
/++ /++
This can be attached to any constructor or function called from the cgi system. This can be attached to any constructor or function called from the cgi system.
@ -8831,6 +8850,74 @@ private bool hasIfCalledFromWeb(attrs...)() {
+/ +/
template AutomaticForm(alias customizer) { } template AutomaticForm(alias customizer) { }
/++
This is meant to be returned by a function that takes a form POST submission. You
want to set the url of the new resource it created, which is set as the http
Location header for a "201 Created" result, and you can also set a separate
destination for browser users, which it sets via a "Refresh" header.
The `resourceRepresentation` should generally be the thing you just created, and
it will be the body of the http response when formatted through the presenter.
The exact thing is up to you - it could just return an id, or the whole object, or
perhaps a partial object.
Examples:
---
class Test : WebObject {
@(Cgi.RequestMethod.POST)
CreatedResource!int makeThing(string value) {
return CreatedResource!int(value.to!int, "/resources/id");
}
}
---
History:
Added December 18, 2021
+/
struct CreatedResource(T) {
static if(!is(T == void))
T resourceRepresentation;
string resourceUrl;
string refreshUrl;
}
/+
/++
This can be attached as a UDA to a handler to add a http Refresh header on a
successful run. (It will not be attached if the function throws an exception.)
This will refresh the browser the given number of seconds after the page loads,
to the url returned by `urlFunc`, which can be either a static function or a
member method of the current handler object.
You might use this for a POST handler that is normally used from ajax, but you
want it to degrade gracefully to a temporarily flashed message before reloading
the main page.
History:
Added December 18, 2021
+/
struct Refresh(alias urlFunc) {
int waitInSeconds;
string url() {
static if(__traits(isStaticFunction, urlFunc))
return urlFunc();
else static if(is(urlFunc : string))
return urlFunc;
}
}
+/
/+
/++
Sets a filter to be run before
A before function can do validations of params and log and stop the function from running.
+/
template Before(alias b) {}
template After(alias b) {}
+/
/+ /+
Argument conversions: for the most part, it is to!Thing(string). Argument conversions: for the most part, it is to!Thing(string).
@ -9081,6 +9168,17 @@ html", true, true);
cgi.setResponseLocation(ret.to, true, getHttpCodeText(ret.code)); cgi.setResponseLocation(ret.to, true, getHttpCodeText(ret.code));
} }
/// [CreatedResource]s send code 201 and will set the given urls, then present the given representation.
void presentSuccessfulReturn(T : CreatedResource!R, Meta, R)(Cgi cgi, T ret, Meta meta, string format) {
cgi.setResponseStatus(getHttpCodeText(201));
if(ret.resourceUrl.length)
cgi.header("Location: " ~ ret.resourceUrl);
if(ret.refreshUrl.length)
cgi.header("Refresh: 0;" ~ ret.refreshUrl);
static if(!is(R == void))
presentSuccessfulReturn(cgi, ret.resourceRepresentation, meta, format);
}
/// Multiple responses deconstruct the algebraic type and forward to the appropriate handler at runtime /// Multiple responses deconstruct the algebraic type and forward to the appropriate handler at runtime
void presentSuccessfulReturn(T : MultipleResponses!Types, Meta, Types...)(Cgi cgi, T ret, Meta meta, string format) { void presentSuccessfulReturn(T : MultipleResponses!Types, Meta, Types...)(Cgi cgi, T ret, Meta meta, string format) {
bool outputted = false; bool outputted = false;
@ -9119,11 +9217,27 @@ html", true, true);
useful forms or richer error messages for the user. useful forms or richer error messages for the user.
+/ +/
void presentExceptionAsHtml(alias func, T)(Cgi cgi, Throwable t, T dg) { void presentExceptionAsHtml(alias func, T)(Cgi cgi, Throwable t, T dg) {
presentExceptionAsHtmlImpl(cgi, t, createAutomaticFormForFunction!(func)(dg)); Form af;
foreach(attr; __traits(getAttributes, func)) {
static if(__traits(isSame, attr, AutomaticForm)) {
af = createAutomaticFormForFunction!(func)(dg);
}
}
presentExceptionAsHtmlImpl(cgi, t, af);
} }
void presentExceptionAsHtmlImpl(Cgi cgi, Throwable t, Form automaticForm) { void presentExceptionAsHtmlImpl(Cgi cgi, Throwable t, Form automaticForm) {
if(auto mae = cast(MissingArgumentException) t) { if(auto e = cast(ResourceNotFoundException) t) {
auto container = this.htmlContainer();
container.addChild("p", e.msg);
if(!cgi.outputtedResponseData)
cgi.setResponseStatus("404 Not Found");
cgi.write(container.parentDocument.toString(), true);
} else if(auto mae = cast(MissingArgumentException) t) {
if(automaticForm is null)
goto generic;
auto container = this.htmlContainer(); auto container = this.htmlContainer();
if(cgi.requestMethod == Cgi.RequestMethod.POST) if(cgi.requestMethod == Cgi.RequestMethod.POST)
container.appendChild(Element.make("p", "Argument `" ~ mae.argumentName ~ "` of type `" ~ mae.argumentType ~ "` is missing")); container.appendChild(Element.make("p", "Argument `" ~ mae.argumentName ~ "` of type `" ~ mae.argumentType ~ "` is missing"));
@ -9131,6 +9245,7 @@ html", true, true);
cgi.write(container.parentDocument.toString(), true); cgi.write(container.parentDocument.toString(), true);
} else { } else {
generic:
auto container = this.htmlContainer(); auto container = this.htmlContainer();
// import std.stdio; writeln(t.toString()); // import std.stdio; writeln(t.toString());
@ -9192,7 +9307,7 @@ html", true, true);
/++ /++
Returns an element for a particular type Returns an element for a particular type
+/ +/
Element elementFor(T)(string displayName, string name) { Element elementFor(T)(string displayName, string name, Element function() udaSuggestion) {
import std.traits; import std.traits;
auto div = Element.make("div"); auto div = Element.make("div");
@ -9218,7 +9333,7 @@ html", true, true);
fieldset.addChild("input", name); fieldset.addChild("input", name);
foreach(idx, memberName; __traits(allMembers, T)) foreach(idx, memberName; __traits(allMembers, T))
static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) { static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) {
fieldset.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(beautify(memberName), name ~ "." ~ memberName)); fieldset.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(beautify(memberName), name ~ "." ~ memberName, null /* FIXME: pull off the UDA */));
} }
} else static if(isSomeString!T || isIntegral!T || isFloatingPoint!T) { } else static if(isSomeString!T || isIntegral!T || isFloatingPoint!T) {
Element lbl; Element lbl;
@ -9229,13 +9344,22 @@ html", true, true);
} else { } else {
lbl = div; lbl = div;
} }
auto i = lbl.addChild("input", name); Element i;
if(udaSuggestion) {
i = udaSuggestion();
lbl.appendChild(i);
} else {
i = lbl.addChild("input", name);
}
i.attrs.name = name; i.attrs.name = name;
static if(isSomeString!T) static if(isSomeString!T)
i.attrs.type = "text"; i.attrs.type = "text";
else else
i.attrs.type = "number"; i.attrs.type = "number";
i.attrs.value = to!string(T.init); if(i.tagName == "textarea")
i.textContent = to!string(T.init);
else
i.attrs.value = to!string(T.init);
} else static if(is(T == bool)) { } else static if(is(T == bool)) {
Element lbl; Element lbl;
if(displayName !is null) { if(displayName !is null) {
@ -9263,7 +9387,7 @@ html", true, true);
i.attrs.type = "file"; i.attrs.type = "file";
} else static if(is(T == K[], K)) { } else static if(is(T == K[], K)) {
auto templ = div.addChild("template"); auto templ = div.addChild("template");
templ.appendChild(elementFor!(K)(null, name)); templ.appendChild(elementFor!(K)(null, name, null /* uda??*/));
if(displayName !is null) if(displayName !is null)
div.addChild("span", displayName, "label-text"); div.addChild("span", displayName, "label-text");
auto btn = div.addChild("button"); auto btn = div.addChild("button");
@ -9312,10 +9436,15 @@ html", true, true);
static if(!mustNotBeSetFromWebParams!(param[0], __traits(getAttributes, param))) { static if(!mustNotBeSetFromWebParams!(param[0], __traits(getAttributes, param))) {
string displayName = beautify(__traits(identifier, param)); string displayName = beautify(__traits(identifier, param));
foreach(attr; __traits(getAttributes, param)) Element function() element;
foreach(attr; __traits(getAttributes, param)) {
static if(is(typeof(attr) == DisplayName)) static if(is(typeof(attr) == DisplayName))
displayName = attr.name; displayName = attr.name;
auto i = form.appendChild(elementFor!(param)(displayName, __traits(identifier, param))); else static if(is(typeof(attr) : typeof(element))) {
element = attr;
}
}
auto i = form.appendChild(elementFor!(param)(displayName, __traits(identifier, param), element));
if(i.querySelector("input[type=file]") !is null) if(i.querySelector("input[type=file]") !is null)
form.setAttribute("enctype", "multipart/form-data"); form.setAttribute("enctype", "multipart/form-data");
} }
@ -9343,10 +9472,13 @@ html", true, true);
foreach(idx, memberName; __traits(derivedMembers, T)) {{ foreach(idx, memberName; __traits(derivedMembers, T)) {{
static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {
string displayName = beautify(memberName); string displayName = beautify(memberName);
Element function() element;
foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName)))
static if(is(typeof(attr) == DisplayName)) static if(is(typeof(attr) == DisplayName))
displayName = attr.name; displayName = attr.name;
form.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(displayName, memberName)); else static if(is(typeof(attr) : typeof(element)))
element = attr;
form.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(displayName, memberName, element));
form.setValue(memberName, to!string(__traits(getMember, obj, memberName))); form.setValue(memberName, to!string(__traits(getMember, obj, memberName)));
}}} }}}
@ -9552,6 +9684,7 @@ struct MultipleResponses(T...) {
+/ +/
} }
// FIXME: implement this somewhere maybe
struct RawResponse { struct RawResponse {
int code; int code;
string[] headers; string[] headers;
@ -9567,7 +9700,7 @@ struct RawResponse {
+/ +/
struct Redirection { struct Redirection {
string to; /// The URL to redirect to. string to; /// The URL to redirect to.
int code = 303; /// The HTTP code to retrn. int code = 303; /// The HTTP code to return.
} }
/++ /++