diff --git a/cgi.d b/cgi.d
index 9ac46b9..1edcdb9 100644
--- a/cgi.d
+++ b/cgi.d
@@ -3294,6 +3294,8 @@ mixin template DispatcherMain(Presenter, DispatcherArgs...) {
/++
Request handler that creates the presenter then forwards to the [dispatcher] function.
Renders 404 if the dispatcher did not handle the request.
+
+ Will automatically serve the presenter.style and presenter.script as "style.css" and "script.js"
+/
void handler(Cgi cgi) {
auto presenter = new Presenter;
@@ -3303,11 +3305,29 @@ mixin template DispatcherMain(Presenter, DispatcherArgs...) {
if(cgi.dispatcher!DispatcherArgs(presenter))
return;
- presenter.renderBasicError(cgi, 404);
+ switch(cgi.pathInfo) {
+ case "/style.css":
+ cgi.setCache(true);
+ cgi.setResponseContentType("text/css");
+ cgi.write(presenter.style(), true);
+ break;
+ case "/script.js":
+ cgi.setCache(true);
+ cgi.setResponseContentType("application/javascript");
+ cgi.write(presenter.script(), true);
+ break;
+ default:
+ presenter.renderBasicError(cgi, 404);
+ }
}
mixin GenericMain!handler;
}
+mixin template DispatcherMain(DispatcherArgs...) if(!is(DispatcherArgs[0] : WebPresenter!T, T)) {
+ class GenericPresenter : WebPresenter!GenericPresenter {}
+ mixin DispatcherMain!(GenericPresenter, DispatcherArgs);
+}
+
private string simpleHtmlEncode(string s) {
return s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\n", "
\n");
}
@@ -4140,12 +4160,20 @@ class CgiFiber : Fiber {
}
void proceed() {
- call();
- auto py = postYield;
- postYield = null;
- if(py !is null)
- py();
+ try {
+ call();
+ auto py = postYield;
+ postYield = null;
+ if(py !is null)
+ py();
+ } catch(Exception e) {
+ if(connection)
+ connection.close();
+ goto terminate;
+ }
+
if(state == State.TERM) {
+ terminate:
import core.memory;
GC.removeRoot(cast(void*) this);
}
@@ -4168,8 +4196,10 @@ void doThreadHttpConnection(CustomCgi, alias fun)(Socket connection) {
void doThreadHttpConnectionGuts(CustomCgi, alias fun, bool alwaysCloseConnection = false)(Socket connection) {
scope(failure) {
// catch all for other errors
- sendAll(connection, plainHttpError(false, "500 Internal Server Error", null));
- connection.close();
+ try {
+ sendAll(connection, plainHttpError(false, "500 Internal Server Error", null));
+ connection.close();
+ } catch(Exception e) {} // swallow it, we're aborting anyway.
}
bool closeConnection = alwaysCloseConnection;
@@ -8466,8 +8496,8 @@ class WebPresenter(CRTP) {
:root {
--mild-border: #ccc;
--middle-border: #999;
- --accent-color: #e8e8e8;
- --sidebar-color: #f2f2f2;
+ --accent-color: #f2f2f2;
+ --sidebar-color: #fefefe;
}
` ~ genericFormStyling() ~ genericSiteStyling();
}
@@ -8988,6 +9018,11 @@ html", true, true);
ol.addChild("li", formatReturnValueAsHtml(e));
return ol;
}
+ } else static if(is(T : Object)) {
+ static if(is(typeof(t.toHtml()))) // FIXME: maybe i will make this an interface
+ return Element.make("div", t.toHtml());
+ else
+ return Element.make("div", t.toString());
} else static assert(0, "bad return value for cgi call " ~ T.stringof);
assert(0);
@@ -9580,6 +9615,8 @@ template urlNamesForMethod(alias method, string default_) {
/++
The base of all REST objects, to be used with [serveRestObject] and [serveRestCollectionOf].
+
+ WARNING: this is not stable.
+/
class RestObject(CRTP) : WebObject {
@@ -9594,21 +9631,24 @@ class RestObject(CRTP) : WebObject {
show();
}
- ValidationResult delegate(typeof(this)) validateFromReflection;
- Element delegate(typeof(this)) toHtmlFromReflection;
- var delegate(typeof(this)) toJsonFromReflection;
-
/// Override this to provide access control to this object.
AccessCheck accessCheck(string urlId, Operation operation) {
return AccessCheck.allowed;
}
ValidationResult validate() {
- if(validateFromReflection !is null)
- return validateFromReflection(this);
+ // FIXME
return ValidationResult.valid;
}
+ string getUrlSlug() {
+ import std.conv;
+ static if(is(typeof(CRTP.id)))
+ return to!string((cast(CRTP) this).id);
+ else
+ return null;
+ }
+
// The functions with more arguments are the low-level ones,
// they forward to the ones with fewer arguments by default.
@@ -9618,7 +9658,9 @@ class RestObject(CRTP) : WebObject {
of the new object.
+/
string create(scope void delegate() applyChanges) {
- return null;
+ applyChanges();
+ save();
+ return getUrlSlug();
}
void replace() {
@@ -9649,18 +9691,31 @@ class RestObject(CRTP) : WebObject {
abstract void load(string urlId);
abstract void save();
- Element toHtml() {
- if(toHtmlFromReflection)
- return toHtmlFromReflection(this);
- else
- assert(0);
+ Element toHtml(Presenter)(Presenter presenter) {
+ import arsd.dom;
+ import std.conv;
+ auto obj = cast(CRTP) this;
+ auto div = Element.make("div");
+ div.addClass("Dclass_" ~ CRTP.stringof);
+ div.dataset.url = getUrlSlug();
+ bool first = true;
+ foreach(idx, memberName; __traits(derivedMembers, CRTP))
+ static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {
+ if(!first) div.addChild("br"); else first = false;
+ div.appendChild(presenter.formatReturnValueAsHtml(__traits(getMember, obj, memberName)));
+ }
+ return div;
}
var toJson() {
- if(toJsonFromReflection)
- return toJsonFromReflection(this);
- else
- assert(0);
+ import arsd.jsvar;
+ var v = var.emptyObject();
+ auto obj = cast(CRTP) this;
+ foreach(idx, memberName; __traits(derivedMembers, CRTP))
+ static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {
+ v[memberName] = __traits(getMember, obj, memberName);
+ }
+ return v;
}
/+
@@ -9889,32 +9944,6 @@ bool restObjectServeHandler(T, Presenter)(Cgi cgi, Presenter presenter, string u
// 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;
- bool first = true;
- foreach(idx, memberName; __traits(derivedMembers, T))
- static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) {
- if(!first) div.addChild("br"); else first = false;
- div.appendChild(presenter.formatReturnValueAsHtml(__traits(getMember, obj, memberName)));
- }
- return div;
- };
- obj.toJsonFromReflection = delegate(t) {
- import arsd.jsvar;
- var v = var.emptyObject();
- 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
@@ -9965,13 +9994,14 @@ bool restObjectServeHandler(T, Presenter)(Cgi cgi, Presenter presenter, string u
`);
else
container.appendHtml(`
+ Back
`);
}
- container.appendChild(obj.toHtml());
+ container.appendChild(obj.toHtml(presenter));
cgi.write(container.parentDocument.toString, true);
}
}
@@ -10162,6 +10192,32 @@ auto serveStaticFile(string urlPrefix, string filename = null, string contentTyp
return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(filename, contentType));
}
+/++
+ Serves static data. To be used with [dispatcher].
+
+ History:
+ Added October 31, 2021
++/
+auto serveStaticData(string urlPrefix, immutable(void)[] data, string contentType = null) {
+ assert(urlPrefix[0] == '/');
+ if(contentType is null) {
+ contentType = contentTypeFromFileExtension(urlPrefix);
+ }
+
+ static struct DispatcherDetails {
+ immutable(void)[] data;
+ string contentType;
+ }
+
+ static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) {
+ cgi.setCache(true);
+ cgi.setResponseContentType(details.contentType);
+ cgi.write(details.data, true);
+ return true;
+ }
+ return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(data, contentType));
+}
+
string contentTypeFromFileExtension(string filename) {
if(filename.endsWith(".png"))
return "image/png";
diff --git a/color.d b/color.d
index cb1fb71..927827a 100644
--- a/color.d
+++ b/color.d
@@ -102,21 +102,20 @@ private {
bool startsWithInternal(in char[] a, in char[] b) {
return (a.length >= b.length && a[0 .. b.length] == b);
}
- inout(char)[][] splitInternal(inout(char)[] a, char c) {
- inout(char)[][] ret;
+ void splitInternal(scope inout(char)[] a, char c, scope void delegate(int, scope inout(char)[]) @safe dg) {
+ int count;
size_t previous = 0;
foreach(i, char ch; a) {
if(ch == c) {
- ret ~= a[previous .. i];
+ dg(count++, a[previous .. i]);
previous = i + 1;
}
}
if(previous != a.length)
- ret ~= a[previous .. $];
- return ret;
+ dg(count++, a[previous .. $]);
}
nothrow @safe @nogc pure
- inout(char)[] stripInternal(inout(char)[] s) {
+ inout(char)[] stripInternal(return inout(char)[] s) {
foreach(i, char c; s)
if(c != ' ' && c != '\t' && c != '\n') {
s = s[i .. $];
@@ -309,13 +308,12 @@ struct Color {
double[3] hsl;
ubyte a = 255;
- auto parts = s.splitInternal(',');
- foreach(i, part; parts) {
+ s.splitInternal(',', (int i, scope const(char)[] part) {
if(i < 3)
hsl[i] = toInternal!double(part.stripInternal);
else
a = clampToByte(cast(int) (toInternal!double(part.stripInternal) * 255));
- }
+ });
c = .fromHsl(hsl);
c.a = a;
@@ -328,8 +326,7 @@ struct Color {
assert(s[$-1] == ')');
s = s[s.startsWithInternal("rgb(") ? 4 : 5 .. $ - 1]; // the closing paren
- auto parts = s.splitInternal(',');
- foreach(i, part; parts) {
+ s.splitInternal(',', (int i, scope const(char)[] part) {
// lol the loop-switch pattern
auto v = toInternal!double(part.stripInternal);
switch(i) {
@@ -347,7 +344,7 @@ struct Color {
break;
default: // ignore
}
- }
+ });
return c;
}
diff --git a/database.d b/database.d
index af6010d..d878d1e 100644
--- a/database.d
+++ b/database.d
@@ -78,11 +78,17 @@ interface Database {
}
import std.stdio;
-Ret queryOneColumn(Ret, string file = __FILE__, size_t line = __LINE__, T...)(Database db, string sql, T t) {
+// Added Oct 26, 2021
+Row queryOneRow(string file = __FILE__, size_t line = __LINE__, T...)(Database db, string sql, T t) {
auto res = db.query(sql, t);
if(res.empty)
throw new Exception("no row in result", file, line);
auto row = res.front;
+ return row;
+}
+
+Ret queryOneColumn(Ret, string file = __FILE__, size_t line = __LINE__, T...)(Database db, string sql, T t) {
+ auto row = queryOneRow(db, sql, t);
return to!Ret(row[0]);
}
@@ -727,19 +733,19 @@ mixin template DataObjectConstructors() {
}
}
-string yield(string what) { return `if(auto result = dg(`~what~`)) return result;`; }
+private string yield(string what) { return `if(auto result = dg(`~what~`)) return result;`; }
import std.typecons;
import std.json; // for json value making
class DataObject {
// lets you just free-form set fields, assuming they all come from the given table
// note it doesn't try to handle joins for new rows. you've gotta do that yourself
- this(Database db, string table) {
+ this(Database db, string table, UpdateOrInsertMode mode = UpdateOrInsertMode.CheckForMe) {
assert(db !is null);
this.db = db;
this.table = table;
- mode = UpdateOrInsertMode.CheckForMe;
+ this.mode = mode;
}
JSONValue makeJsonValue() {
@@ -1103,19 +1109,24 @@ class SimpleDataObject(string tableToUse, fieldsToUse) : DataObject {
break on complex tables.
Data types handled:
+
+ ```
INTEGER, SMALLINT, MEDIUMINT -> D's int
TINYINT -> D's bool
BIGINT -> D's long
TEXT, VARCHAR -> D's string
FLOAT, DOUBLE -> D's double
+ ```
It also reads DEFAULT values to pass to D, except for NULL.
It ignores any length restrictions.
Bugs:
- Skips all constraints
- Doesn't handle nullable fields, except with strings
- It only handles SQL keywords if they are all caps
+ $(LIST
+ * Skips all constraints
+ * Doesn't handle nullable fields, except with strings
+ * It only handles SQL keywords if they are all caps
+ )
This, when combined with SimpleDataObject!(),
can automatically create usable D classes from
@@ -1206,6 +1217,7 @@ string getCreateTable(string sql, string tableName) {
case "INTEGER":
case "SMALLINT":
case "MEDIUMINT":
+ case "SERIAL": // added Oct 23, 2021
structCode ~= "int";
break;
case "BOOLEAN":
@@ -1221,6 +1233,7 @@ string getCreateTable(string sql, string tableName) {
case "varchar":
case "TEXT":
case "text":
+ case "TIMESTAMPTZ": // added Oct 23, 2021
structCode ~= "string";
break;
case "FLOAT":
@@ -1352,15 +1365,70 @@ mixin template DatabaseOperations(string table) {
}
+string toDbName(string s) {
+ import std.string;
+ return s.toLower ~ "s";
+}
+
+/++
+ Easy interop with [arsd.cgi] serveRestObject classes.
+
+ History:
+ Added October 31, 2021.
+
+ Warning: not stable/supported at this time.
++/
+mixin template DatabaseRestObject(alias getDb) {
+ override void save() {
+ this.id = this.saveToDatabase(getDb());
+ }
+
+ override void load(string urlId) {
+ import std.conv;
+ this.id = to!int(urlId);
+ this.loadFromDatabase(getDb());
+ }
+}
+
+void loadFromDatabase(T)(T t, Database database, string tableName = toDbName(__traits(identifier, T))) {
+ static assert(is(T == class), "structs wont work for this function, try rowToObject instead for now and complain to me adding struct support is easy enough");
+ auto query = new SelectBuilder(database);
+ query.table = tableName;
+ query.fields ~= "*";
+ query.wheres ~= "id = ?0";
+ auto res = database.query(query.toString(), t.id);
+ if(res.empty)
+ throw new Exception("no such row");
+
+ rowToObject(res.front, t);
+}
+
+auto saveToDatabase(T)(T t, Database database, string tableName = toDbName(__traits(identifier, T))) {
+ DataObject obj = objectToDataObject(t, database, tableName, t.id ? UpdateOrInsertMode.AlwaysUpdate : UpdateOrInsertMode.AlwaysInsert);
+ if(!t.id) {
+ import std.random; // omg i hate htis
+ obj.id = uniform(2, int.max);
+ }
+ obj.commitChanges;
+ return t.id;
+}
+
import std.traits, std.datetime;
enum DbSave;
enum DbNullable;
alias AliasHelper(alias T) = T;
T rowToObject(T)(Row row) {
+ T t;
+ static if(is(T == class))
+ t = new T();
+ rowToObject(row, t);
+ return t;
+}
+
+void rowToObject(T)(Row row, ref T t) {
import arsd.dom, arsd.cgi;
- T t;
foreach(memberName; __traits(allMembers, T)) {
alias member = AliasHelper!(__traits(getMember, t, memberName));
foreach(attr; __traits(getAttributes, member)) {
@@ -1381,14 +1449,12 @@ T rowToObject(T)(Row row) {
}
}
}
- return t;
-
}
-DataObject objectToDataObject(T)(T t, Database db, string table) {
+DataObject objectToDataObject(T)(T t, Database db, string table, UpdateOrInsertMode mode = UpdateOrInsertMode.CheckForMe) {
import arsd.dom, arsd.cgi;
- DataObject obj = new DataObject(db, table);
+ DataObject obj = new DataObject(db, table, mode);
foreach(memberName; __traits(allMembers, T)) {
alias member = AliasHelper!(__traits(getMember, t, memberName));
foreach(attr; __traits(getAttributes, member)) {
@@ -1408,8 +1474,18 @@ DataObject objectToDataObject(T)(T t, Database db, string table) {
}
}
- if(!done)
- obj.opDispatch!memberName(__traits(getMember, t, memberName));
+ if(!done) {
+ static if(memberName == "id") {
+ if(__traits(getMember, t, memberName)) {
+ // maybe i shouldn't actually set the id but idk
+ obj.opDispatch!memberName(__traits(getMember, t, memberName));
+ } else {
+ // it is null, let the system do something about it like auto increment
+
+ }
+ } else
+ obj.opDispatch!memberName(__traits(getMember, t, memberName));
+ }
}
}
}
diff --git a/dom.d b/dom.d
index 485cd12..0648e02 100644
--- a/dom.d
+++ b/dom.d
@@ -6228,6 +6228,17 @@ int intFromHex(string hex) {
// a "*" matcher to act as a root. for cases like document.querySelector("> body")
// which implies html
+ // however, if it is a child-matching selector and there are no children,
+ // bail out early as it obviously cannot match.
+ bool hasNonTextChildren = false;
+ foreach(c; e.children)
+ if(c.nodeType != 3) {
+ hasNonTextChildren = true;
+ break;
+ }
+ if(!hasNonTextChildren)
+ return false;
+
// there is probably a MUCH better way to do this.
auto dummy = SelectorPart.init;
dummy.tagNameFilter = "*";
@@ -7804,11 +7815,22 @@ unittest {
Foo
Bar
+
+ test