Merge branch 'master' of github.com:adamdruppe/arsd

This commit is contained in:
Adam D. Ruppe 2019-05-11 10:28:38 -04:00
commit 427b325296
8 changed files with 154 additions and 49 deletions

97
cgi.d
View File

@ -1107,7 +1107,6 @@ class Cgi {
} }
/// ///
void writeToFile(string filenameToSaveTo) const { void writeToFile(string filenameToSaveTo) const {
import std.file; import std.file;
@ -5272,7 +5271,7 @@ ScheduledJobHelper schedule(alias fn, T...)(T args) {
/// ///
interface ScheduledJobServer { interface ScheduledJobServer {
/// /// Use the [schedule] function for a higher-level interface.
int scheduleJob(int whenIs, int when, string executable, string func, string[] args); int scheduleJob(int whenIs, int when, string executable, string func, string[] args);
/// ///
void cancelJob(int jobId); void cancelJob(int jobId);
@ -5282,6 +5281,16 @@ class ScheduledJobServerConnection : ScheduledJobServer {
mixin ImplementRpcClientInterface!(ScheduledJobServer, "/tmp/arsd_scheduled_job_server"); mixin ImplementRpcClientInterface!(ScheduledJobServer, "/tmp/arsd_scheduled_job_server");
} }
final class ScheduledJobServerImplementation : ScheduledJobServer {
protected int scheduleJob(int whenIs, int when, string executable, string func, string[] args) {
return 0;
}
protected void cancelJob(int jobId) {
}
}
/// ///
interface EventSourceServer { interface EventSourceServer {
/++ /++
@ -6044,6 +6053,18 @@ static auto elementFor(T)(string displayName, string name) {
else else
i.attrs.type = "number"; i.attrs.type = "number";
i.attrs.value = to!string(T.init); i.attrs.value = to!string(T.init);
} else static if(is(T == bool)) {
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.type = "checkbox";
i.attrs.name = name;
} 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));
@ -6370,6 +6391,8 @@ auto formatReturnValueAsHtml(T)(T t) {
} }
return dl; return dl;
} else static if(is(T == bool)) {
return Element.make("span", t ? "true" : "false", "automatic-data-display");
} else static if(is(T == E[], E)) { } else static if(is(T == E[], E)) {
static if(is(E : RestObject!Proxy, Proxy)) { static if(is(E : RestObject!Proxy, Proxy)) {
// treat RestObject similar to struct // treat RestObject similar to struct
@ -6439,18 +6462,6 @@ auto formatReturnValueAsHtml(T)(T t) {
FIXME FIXME
+/ +/
class WebPresenter() { class WebPresenter() {
}
/++
The base class for the [dispatcher] function and object support.
+/
class WebObject(Helper = void) {
Cgi cgi;
void initialize(Cgi cgi) {
this.cgi = cgi;
}
string script() { string script() {
return ` return `
`; `;
@ -6569,6 +6580,19 @@ class WebObject(Helper = void) {
} }
} }
/++
The base class for the [dispatcher] function and object support.
+/
class WebObject(Helper = void) {
Cgi cgi;
WebPresenter!() presenter;
void initialize(Cgi cgi, WebPresenter!() presenter) {
this.cgi = cgi;
this.presenter = presenter;
}
}
/++ /++
Serves a class' methods, as a kind of low-state RPC over the web. To be used with [dispatcher]. Serves a class' methods, as a kind of low-state RPC over the web. To be used with [dispatcher].
@ -6582,10 +6606,10 @@ auto serveApi(T)(string urlPrefix) {
import arsd.dom; import arsd.dom;
import arsd.jsvar; import arsd.jsvar;
static bool handler(string urlPrefix, Cgi cgi, DispatcherDetails details) { static bool handler(string urlPrefix, Cgi cgi, WebPresenter!() presenter, DispatcherDetails details) {
auto obj = new T(); auto obj = new T();
obj.initialize(cgi); obj.initialize(cgi, presenter);
switch(cgi.pathInfo[urlPrefix.length .. $]) { switch(cgi.pathInfo[urlPrefix.length .. $]) {
static foreach(methodName; __traits(derivedMembers, T)){{ static foreach(methodName; __traits(derivedMembers, T)){{
@ -6594,7 +6618,7 @@ auto serveApi(T)(string urlPrefix) {
case urlify(methodName): case urlify(methodName):
switch(cgi.request("format", "html")) { switch(cgi.request("format", "html")) {
case "html": case "html":
auto container = obj.htmlContainer(); auto container = obj.presenter.htmlContainer();
try { try {
auto ret = callFromCgi!(__traits(getMember, obj, methodName))(&__traits(getMember, obj, methodName), cgi); auto ret = callFromCgi!(__traits(getMember, obj, methodName))(&__traits(getMember, obj, methodName), cgi);
container.appendChild(formatReturnValueAsHtml(ret)); container.appendChild(formatReturnValueAsHtml(ret));
@ -6623,12 +6647,12 @@ auto serveApi(T)(string urlPrefix) {
case "script.js": case "script.js":
cgi.setResponseContentType("text/javascript"); cgi.setResponseContentType("text/javascript");
cgi.gzipResponse = true; cgi.gzipResponse = true;
cgi.write(obj.script(), true); cgi.write(obj.presenter.script(), true);
return true; return true;
case "style.css": case "style.css":
cgi.setResponseContentType("text/css"); cgi.setResponseContentType("text/css");
cgi.gzipResponse = true; cgi.gzipResponse = true;
cgi.write(obj.style(), true); cgi.write(obj.presenter.style(), true);
return true; return true;
default: default:
return false; return false;
@ -6768,6 +6792,9 @@ mixin template Presenter() {
} }
// FIXME XSRF token, prolly can just put in a cookie and then it needs to be copied to header or form hidden value
// https://use-the-index-luke.com/sql/partial-results/fetch-next-page
/++ /++
Base class for REST collections. Base class for REST collections.
+/ +/
@ -6934,7 +6961,7 @@ class CollectionOf(Obj, Helper = void) : RestObject!(Helper) {
+/ +/
auto serveRestObject(T)(string urlPrefix) { auto serveRestObject(T)(string urlPrefix) {
assert(urlPrefix[$ - 1] != '/', "Do NOT use a trailing slash on REST objects."); assert(urlPrefix[$ - 1] != '/', "Do NOT use a trailing slash on REST objects.");
static bool handler(string urlPrefix, Cgi cgi, DispatcherDetails details) { static bool handler(string urlPrefix, Cgi cgi, WebPresenter!() presenter, DispatcherDetails details) {
string url = cgi.pathInfo[urlPrefix.length .. $]; string url = cgi.pathInfo[urlPrefix.length .. $];
if(url.length && url[$ - 1] == '/') { if(url.length && url[$ - 1] == '/') {
@ -6943,7 +6970,7 @@ auto serveRestObject(T)(string urlPrefix) {
return true; return true;
} }
return restObjectServeHandler!T(cgi, url); return restObjectServeHandler!T(cgi, presenter, url);
} }
return DispatcherDefinition!handler(urlPrefix, false); return DispatcherDefinition!handler(urlPrefix, false);
@ -6957,7 +6984,7 @@ auto serveRestCollectionOf(T)(string urlPrefix) {
return serveRestObject!(mixin(T.stringof ~ "s"))(urlPrefix); return serveRestObject!(mixin(T.stringof ~ "s"))(urlPrefix);
} }
bool restObjectServeHandler(T)(Cgi cgi, string url) { bool restObjectServeHandler(T)(Cgi cgi, WebPresenter!() presenter, string url) {
string urlId = null; string urlId = null;
if(url.length && url[0] == '/') { if(url.length && url[0] == '/') {
// asking for a subobject // asking for a subobject
@ -6974,7 +7001,7 @@ bool restObjectServeHandler(T)(Cgi cgi, string url) {
static if(is(T : CollectionOf!(C, P), C, P)) { static if(is(T : CollectionOf!(C, P), C, P)) {
if(urlId !is null) { if(urlId !is null) {
return restObjectServeHandler!C(cgi, url); // FIXME? urlId); return restObjectServeHandler!C(cgi, presenter, url); // FIXME? urlId);
} }
} }
@ -7007,7 +7034,7 @@ bool restObjectServeHandler(T)(Cgi cgi, string url) {
// FIXME // FIXME
return ValidationResult.valid; return ValidationResult.valid;
}; };
obj.initialize(cgi); obj.initialize(cgi, presenter);
// FIXME: populate reflection info delegates // FIXME: populate reflection info delegates
@ -7016,12 +7043,12 @@ bool restObjectServeHandler(T)(Cgi cgi, string url) {
case "script.js": case "script.js":
cgi.setResponseContentType("text/javascript"); cgi.setResponseContentType("text/javascript");
cgi.gzipResponse = true; cgi.gzipResponse = true;
cgi.write(obj.script(), true); cgi.write(obj.presenter.script(), true);
return true; return true;
case "style.css": case "style.css":
cgi.setResponseContentType("text/css"); cgi.setResponseContentType("text/css");
cgi.gzipResponse = true; cgi.gzipResponse = true;
cgi.write(obj.style(), true); cgi.write(obj.presenter.style(), true);
return true; return true;
default: default:
// intentionally blank // intentionally blank
@ -7047,7 +7074,7 @@ bool restObjectServeHandler(T)(Cgi cgi, string url) {
cgi.setResponseContentType("application/json"); cgi.setResponseContentType("application/json");
cgi.write(obj.toJson().toString, true); cgi.write(obj.toJson().toString, true);
} else { } else {
auto container = obj.htmlContainer(); auto container = obj.presenter.htmlContainer();
if(addFormLinks) { if(addFormLinks) {
static if(is(T : CollectionOf!(C, P), C, P)) static if(is(T : CollectionOf!(C, P), C, P))
container.appendHtml(` container.appendHtml(`
@ -7083,7 +7110,7 @@ bool restObjectServeHandler(T)(Cgi cgi, string url) {
static if(is(T : CollectionOf!(C, P), C, P)) { static if(is(T : CollectionOf!(C, P), C, P)) {
auto results = obj.index(); auto results = obj.index();
if(cgi.request("format", "html") == "html") { if(cgi.request("format", "html") == "html") {
auto container = obj.htmlContainer(); auto container = obj.presenter.htmlContainer();
auto html = formatReturnValueAsHtml(results.results); auto html = formatReturnValueAsHtml(results.results);
container.appendHtml(` container.appendHtml(`
<form> <form>
@ -7119,7 +7146,7 @@ bool restObjectServeHandler(T)(Cgi cgi, string url) {
case "PUT": case "PUT":
case "POST": case "POST":
// an editing form for the object // an editing form for the object
auto container = obj.htmlContainer(); auto container = obj.presenter.htmlContainer();
static if(__traits(compiles, () { auto o = new obj.PostProxy(); })) { static if(__traits(compiles, () { auto o = new obj.PostProxy(); })) {
auto form = (cgi.request("_method") == "POST") ? createAutomaticFormForObject(new obj.PostProxy()) : createAutomaticFormForObject(obj); auto form = (cgi.request("_method") == "POST") ? createAutomaticFormForObject(new obj.PostProxy()) : createAutomaticFormForObject(obj);
} else { } else {
@ -7132,7 +7159,7 @@ bool restObjectServeHandler(T)(Cgi cgi, string url) {
break; break;
case "DELETE": case "DELETE":
// FIXME: a delete form for the object (can be phrased "are you sure?") // FIXME: a delete form for the object (can be phrased "are you sure?")
auto container = obj.htmlContainer(); auto container = obj.presenter.htmlContainer();
container.appendHtml(` container.appendHtml(`
<form method="POST"> <form method="POST">
Are you sure you want to delete this item? Are you sure you want to delete this item?
@ -7237,7 +7264,7 @@ auto serveStaticFile(string urlPrefix, string filename = null, string contentTyp
contentType = "application/javascript"; contentType = "application/javascript";
} }
static bool handler(string urlPrefix, Cgi cgi, DispatcherDetails details) { static bool handler(string urlPrefix, Cgi cgi, WebPresenter!() presenter, DispatcherDetails details) {
if(details.contentType.indexOf("image/") == 0) if(details.contentType.indexOf("image/") == 0)
cgi.setCache(true); cgi.setCache(true);
cgi.setResponseContentType(details.contentType); cgi.setResponseContentType(details.contentType);
@ -7271,17 +7298,19 @@ auto serveStaticData(string urlPrefix, const(void)[] data, string contentType) {
)) return; )) return;
--- ---
+/ +/
bool dispatcher(definitions...)(Cgi cgi) { bool dispatcher(definitions...)(Cgi cgi, WebPresenter!() presenter = null) {
if(presenter is null)
presenter = new WebPresenter!();
// I can prolly make this more efficient later but meh. // I can prolly make this more efficient later but meh.
static foreach(definition; definitions) { static foreach(definition; definitions) {
if(definition.rejectFurther) { if(definition.rejectFurther) {
if(cgi.pathInfo == definition.urlPrefix) { if(cgi.pathInfo == definition.urlPrefix) {
auto ret = definition.handler(definition.urlPrefix, cgi, definition.details); auto ret = definition.handler(definition.urlPrefix, cgi, presenter, definition.details);
if(ret) if(ret)
return true; return true;
} }
} else if(cgi.pathInfo.startsWith(definition.urlPrefix)) { } else if(cgi.pathInfo.startsWith(definition.urlPrefix)) {
auto ret = definition.handler(definition.urlPrefix, cgi, definition.details); auto ret = definition.handler(definition.urlPrefix, cgi, presenter, definition.details);
if(ret) if(ret)
return true; return true;
} }

3
dom.d
View File

@ -3,6 +3,9 @@
// FIXME: https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML // FIXME: https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
// FIXME: appendChild should not fail if the thing already has a parent; it should just automatically remove it per standard. // FIXME: appendChild should not fail if the thing already has a parent; it should just automatically remove it per standard.
// xml entity references?!
/++ /++
This is an html DOM implementation, started with cloning This is an html DOM implementation, started with cloning
what the browser offers in Javascript, but going well beyond what the browser offers in Javascript, but going well beyond

31
email.d
View File

@ -26,6 +26,13 @@ struct MimeAttachment {
string id; /// string id; ///
} }
///
enum ToType {
to,
cc,
bcc
}
/++ /++
For OUTGOING email For OUTGOING email
@ -65,12 +72,32 @@ class EmailMessage {
private bool isMime = false; private bool isMime = false;
private bool isHtml = false; private bool isHtml = false;
///
void addRecipient(string name, string email, ToType how = ToType.to) {
addRecipient(`"`~name~`" <`~email~`>`, how);
}
///
void addRecipient(string who, ToType how = ToType.to) {
final switch(how) {
case ToType.to:
to ~= who;
break;
case ToType.cc:
cc ~= who;
break;
case ToType.bcc:
bcc ~= who;
break;
}
}
/// ///
void setTextBody(string text) { void setTextBody(string text) {
textBody = text; textBody = text.strip;
} }
/// automatically sets a text fallback if you haven't already /// automatically sets a text fallback if you haven't already
void setHtmlBody(string html) { void setHtmlBody()(string html) {
isMime = true; isMime = true;
isHtml = true; isHtml = true;
htmlBody = html; htmlBody = html;

View File

@ -1672,7 +1672,7 @@ class HttpApiClient() {
/// ///
var throwOnError(HttpResponse res) { var throwOnError(HttpResponse res) {
if(res.code < 200 || res.code >= 300) if(res.code < 200 || res.code >= 300)
throw new Exception(res.codeText); throw new Exception(res.codeText ~ " " ~ res.contentText);
var response = var.fromJson(res.contentText); var response = var.fromJson(res.contentText);
if(response.errors) { if(response.errors) {

View File

@ -8,6 +8,13 @@
// https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw // https://docs.microsoft.com/en-us/windows/desktop/Controls/about-custom-draw
// FIXME: make the scroll thing go to bottom when the content changes.
// add a knob slider view... you click and go up and down so basically same as a vertical slider, just presented as a round image
// FIXME: the scroll area MUST be fixed to use the proper apis under the hood.
// FIXME: add a command search thingy built in and implement tip. // FIXME: add a command search thingy built in and implement tip.
// FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?! // FIXME: omg omg what if menu functions have arguments and it can pop up a gui or command line script them?!

View File

@ -142,6 +142,11 @@ $(SIDE_BY_SIDE
) )
) )
$(TIP
If you are going to use the library with a SDL OpenGL context,
try working with a backwards compatible context profile.
)
Creating drawing context Creating drawing context
======================== ========================

View File

@ -1976,6 +1976,8 @@ Expression parsePart(MyTokenStreamHere)(ref MyTokenStreamHere tokens) {
} }
} }
// FIXME: unary ! doesn't work right
funcLoop: while(!tokens.empty) { funcLoop: while(!tokens.empty) {
auto peek = tokens.front; auto peek = tokens.front;
if(peek.type == ScriptToken.Type.symbol) { if(peek.type == ScriptToken.Type.symbol) {

56
web.d
View File

@ -2533,7 +2533,7 @@ class NoSuchFunctionException : NoSuchPageException {
type fromUrlParam(type)(in string ofInterest, in string name, in string[][string] all) { type fromUrlParam(type)(in string ofInterest, in string name, in string[][string] all) {
type ret; type ret;
static if(isArray!(type) && !isSomeString!(type)) { static if(!is(type == enum) && isArray!(type) && !isSomeString!(type)) {
// how do we get an array out of a simple string? // how do we get an array out of a simple string?
// FIXME // FIXME
static assert(0); static assert(0);
@ -2561,6 +2561,17 @@ type fromUrlParam(type)(in string ofInterest, in string name, in string[][string
if(lol in all) if(lol in all)
ret.tupleof[idx] = fromUrlParam!(typeof(thing))(all[lol], lol, all); ret.tupleof[idx] = fromUrlParam!(typeof(thing))(all[lol], lol, all);
} }
} else static if(is(type == enum)) {
sw: switch(ofInterest) {
static foreach(N; __traits(allMembers, type)) {
case N:
ret = __traits(getMember, type, N);
break sw;
}
default:
throw new InvalidParameterException(name, ofInterest, "");
}
} }
/* /*
else static if(is(type : struct)) { else static if(is(type : struct)) {
@ -2568,7 +2579,6 @@ type fromUrlParam(type)(in string ofInterest, in string name, in string[][string
} }
*/ */
else { else {
// enum should be handled by this too
ret = to!type(ofInterest); ret = to!type(ofInterest);
} // FIXME: can we support classes? } // FIXME: can we support classes?
@ -2580,7 +2590,7 @@ type fromUrlParam(type)(in string[] ofInterest, in string name, in string[][stri
type ret; type ret;
// Arrays in a query string are sent as the name repeating... // Arrays in a query string are sent as the name repeating...
static if(isArray!(type) && !isSomeString!type) { static if(!is(type == enum) && isArray!(type) && !isSomeString!type) {
foreach(a; ofInterest) { foreach(a; ofInterest) {
ret ~= fromUrlParam!(ElementType!(type))(a, name, all); ret ~= fromUrlParam!(ElementType!(type))(a, name, all);
} }
@ -2770,6 +2780,11 @@ string formatAs(T, R)(T ret, string format, R api = null, JSONValue* returnValue
} else { } else {
format = "html"; format = "html";
} }
static if(is(typeof(ret) : K[], K)) {
static if(is(K == struct))
format = "table";
}
} }
string retstr; string retstr;
@ -2814,9 +2829,15 @@ string formatAs(T, R)(T ret, string format, R api = null, JSONValue* returnValue
void gotATable(Table table) { void gotATable(Table table) {
if(format == "csv") { if(format == "csv") {
retstr = tableToCsv(table); retstr = tableToCsv(table);
} else if(format == "table") } else if(format == "table") {
retstr = table.toString(); auto div = Element.make("div");
else assert(0); if(api !is null) {
auto cgi = api.cgi;
div.addChild("a", "Download as CSV", cgi.pathInfo ~ "?" ~ cgi.queryString ~ "&format=csv&envelopeFormat=csv");
}
div.appendChild(table);
retstr = div.toString();
} else assert(0);
if(returnValue !is null) if(returnValue !is null)
@ -2837,10 +2858,13 @@ string formatAs(T, R)(T ret, string format, R api = null, JSONValue* returnValue
} else static if(is(typeof(ret) : K[N][V], size_t N, K, V)) { } else static if(is(typeof(ret) : K[N][V], size_t N, K, V)) {
auto table = cast(Table) Element.make("table"); auto table = cast(Table) Element.make("table");
table.addClass("data-display"); table.addClass("data-display");
auto headerRow = table.addChild("tr");
foreach(n; 0 .. N)
table.addChild("th", "" ~ cast(char)(n + 'A'));
foreach(k, v; ret) { foreach(k, v; ret) {
auto row = table.addChild("tr"); auto row = table.addChild("tr");
foreach(cell; v) foreach(cell; v)
table.addChild("td", to!string(cell)); row.addChild("td", to!string(cell));
} }
gotATable(table); gotATable(table);
break; break;
@ -2936,6 +2960,9 @@ string toUrlName(string name) {
string beautify(string name) { string beautify(string name) {
string n; string n;
// really if this is cap and the following is lower, we want a space.
// or in other words, if this is lower and previous is cap, we want a space injected before previous
// all caps names shouldn't get spaces // all caps names shouldn't get spaces
if(name.length == 0 || name.toUpper() == name) if(name.length == 0 || name.toUpper() == name)
return name; return name;
@ -2943,8 +2970,11 @@ string beautify(string name) {
n ~= toUpper(name[0..1]); n ~= toUpper(name[0..1]);
dchar last; dchar last;
foreach(dchar c; name[1..$]) { foreach(idx, dchar c; name[1..$]) {
if((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) { if(c >= 'A' && c <= 'Z') {
if(idx + 1 < name[1 .. $].length && name[1 + idx + 1] >= 'a' && name[1 + idx + 1] <= 'z')
n ~= " ";
} else if(c >= '0' && c <= '9') {
if(last != ' ') if(last != ' ')
n ~= " "; n ~= " ";
} }
@ -3974,9 +4004,11 @@ Table structToTable(T)(Document document, T arr, string[] fieldsToSkip = null) i
{ {
auto thead = t.addChild("thead"); auto thead = t.addChild("thead");
auto tr = thead.addChild("tr"); auto tr = thead.addChild("tr");
auto s = arr[0]; if(arr.length) {
foreach(idx, member; s.tupleof) auto s = arr[0];
tr.addChild("th", s.tupleof[idx].stringof[2..$]); foreach(idx, member; s.tupleof)
tr.addChild("th", s.tupleof[idx].stringof[2..$]);
}
} }
bool odd = true; bool odd = true;