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

This commit is contained in:
Adam D. Ruppe 2018-11-10 21:30:05 -05:00
commit 9cb2c9f539
8 changed files with 260 additions and 163 deletions

243
cgi.d
View File

@ -1,12 +1,33 @@
// 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: new ConnectionThread is done a lot, no pooling implemented
// Note: spawn-fcgi can help with fastcgi on nginx // Note: spawn-fcgi can help with fastcgi on nginx
// FIXME: to do: add openssl optionally // FIXME: to do: add openssl optionally
// make sure embedded_httpd doesn't send two answers if one writes() then dies // make sure embedded_httpd doesn't send two answers if one writes() then dies
// future direction: websocket as a separate process that you can sendfile to for an async passoff of those long-lived connections
/*
Session manager process: it spawns a new process, passing a
command line argument, to just be a little key/value store
of some serializable struct. On Windows, it CreateProcess.
On Linux, it can just fork or maybe fork/exec. The session
key is in a cookie.
Server-side event process: spawns an async manager. You can
push stuff out to channel ids and the clients listen to it.
websocket process: spawns an async handler. They can talk to
each other or get info from a cgi request.
Tempting to put web.d 2.0 in here. It would:
* map urls and form generation to functions
* have data presentation magic
* do the skeleton stuff like 1.0
* auto-cache generated stuff in files (at least if pure?)
*/
/++ /++
Provides a uniform server-side API for CGI, FastCGI, SCGI, and HTTP web applications. Provides a uniform server-side API for CGI, FastCGI, SCGI, and HTTP web applications.
@ -1518,8 +1539,10 @@ class Cgi {
if(header.indexOf("HTTP/1.0") != -1) { if(header.indexOf("HTTP/1.0") != -1) {
http10 = true; http10 = true;
autoBuffer = true; autoBuffer = true;
if(closeConnection) if(closeConnection) {
// on http 1.0, close is assumed (unlike http/1.1 where we assume keep alive)
*closeConnection = true; *closeConnection = true;
}
} }
} else { } else {
// other header // other header
@ -1540,8 +1563,16 @@ class Cgi {
else if (name == "connection") { else if (name == "connection") {
if(value == "close" && closeConnection) if(value == "close" && closeConnection)
*closeConnection = true; *closeConnection = true;
if(value.toLower().indexOf("keep-alive") != -1) if(value.toLower().indexOf("keep-alive") != -1) {
keepAliveRequested = true; keepAliveRequested = true;
// on http 1.0, the connection is closed by default,
// but not if they request keep-alive. then we don't close
// anymore - undoing the set above
if(http10 && closeConnection) {
*closeConnection = false;
}
}
} }
else if (name == "transfer-encoding") { else if (name == "transfer-encoding") {
if(value == "chunked") if(value == "chunked")
@ -2737,6 +2768,9 @@ mixin template CustomCgiMainImpl(CustomCgi, alias fun, long maxContentLength = d
throw new Exception("bind"); throw new Exception("bind");
} }
// FIXME: if this queue is full, it will just ignore it
// and wait for the client to retransmit it. This is an
// obnoxious timeout condition there.
if(sock.listen(128) == -1) { if(sock.listen(128) == -1) {
close(sock); close(sock);
throw new Exception("listen"); throw new Exception("listen");
@ -2968,6 +3002,9 @@ void doThreadHttpConnection(CustomCgi, alias fun)(Socket connection) {
sendAll(connection, plainHttpError(false, "500 Internal Server Error", null)); sendAll(connection, plainHttpError(false, "500 Internal Server Error", null));
connection.close(); connection.close();
} }
connection.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10));
bool closeConnection; bool closeConnection;
auto ir = new BufferedInputRange(connection); auto ir = new BufferedInputRange(connection);
@ -3100,6 +3137,7 @@ void doThreadScgiConnection(CustomCgi, alias fun, long maxContentLength)(Socket
try { try {
fun(cgi); fun(cgi);
cgi.close(); cgi.close();
connection.close();
} catch(Throwable t) { } catch(Throwable t) {
// no std err // no std err
if(!handleException(cgi, t)) { if(!handleException(cgi, t)) {
@ -3316,6 +3354,11 @@ class BufferedInputRange {
try_again: try_again:
auto ret = source.receive(freeSpace); auto ret = source.receive(freeSpace);
if(ret == Socket.ERROR) { if(ret == Socket.ERROR) {
if(wouldHaveBlocked()) {
// gonna treat a timeout here as a close
sourceClosed = true;
return;
}
version(Posix) { version(Posix) {
import core.stdc.errno; import core.stdc.errno;
if(errno == EINTR || errno == EAGAIN) { if(errno == EINTR || errno == EAGAIN) {
@ -3374,32 +3417,8 @@ class BufferedInputRange {
bool sourceClosed; bool sourceClosed;
} }
class ConnectionThread2 : Thread { import core.sync.semaphore;
import std.concurrency; import core.atomic;
this(void function(Socket) handler) {
this.handler = handler;
super(&run);
}
void run() {
tid = thisTid();
available = true;
while(true)
receive(
(/*Socket*/ size_t s) {
available = false;
try {
handler(cast(Socket) cast(void*) s);
} catch(Throwable t) {}
available = true;
}
);
}
bool available;
Tid tid;
void function(Socket) handler;
}
/** /**
To use this thing: To use this thing:
@ -3415,38 +3434,61 @@ class ConnectionThread2 : Thread {
FIXME: should I offer an event based async thing like netman did too? Yeah, probably. FIXME: should I offer an event based async thing like netman did too? Yeah, probably.
*/ */
class ListeningConnectionManager { class ListeningConnectionManager {
Semaphore semaphore;
Socket[256] queue;
shared(ubyte) nextIndexFront;
ubyte nextIndexBack;
shared(int) queueLength;
void listen() { void listen() {
version(cgi_multiple_connections_per_thread) { running = true;
import std.concurrency; shared(int) loopBroken;
import std.random;
ConnectionThread2[16] pool;
foreach(ref p; pool) {
p = new ConnectionThread2(handler);
p.start();
}
while(true) { version(cgi_no_threads) {
auto connection = listener.accept(); // NEVER USE THIS
// it exists only for debugging and other special occasions
bool handled = false; // the thread mode is faster and less likely to stall the whole
retry: // thing when a request is slow
foreach(p; pool) while(!loopBroken && running) {
if(p.available) { auto sn = listener.accept();
handled = true; try {
send(p.tid, cast(size_t) cast(void*) connection); handler(sn);
break; } catch(Exception e) {
} // if a connection goes wrong, we want to just say no, but try to carry on unless it is an Error of some sort (in which case, we'll die. You might want an external helper program to revive the server when it dies)
sn.close();
// none available right now, make it wait a bit then try again
if(!handled) {
Thread.sleep(dur!"msecs"(25));
goto retry;
} }
} }
} else { } else {
foreach(connection; this) semaphore = new Semaphore();
handler(connection);
ConnectionThread[16] threads;
foreach(ref thread; threads) {
thread = new ConnectionThread(this, handler);
thread.start();
}
while(!loopBroken && running) {
auto sn = listener.accept();
// disable Nagle's algorithm to avoid a 40ms delay when we send/recv
// on the socket because we do some buffering internally. I think this helps,
// certainly does for small requests, and I think it does for larger ones too
sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1);
while(queueLength >= queue.length)
Thread.sleep(1.msecs);
synchronized(this) {
queue[nextIndexBack] = sn;
nextIndexBack++;
atomicOp!"+="(queueLength, 1);
}
semaphore.notify();
foreach(thread; threads) {
if(!thread.isRunning) {
thread.join();
}
}
}
} }
} }
@ -3465,50 +3507,6 @@ class ListeningConnectionManager {
void quit() { void quit() {
running = false; running = false;
} }
int opApply(scope CMT dg) {
running = true;
shared(int) loopBroken;
while(!loopBroken && running) {
auto sn = listener.accept();
try {
version(cgi_no_threads) {
// NEVER USE THIS
// it exists only for debugging and other special occasions
// the thread mode is faster and less likely to stall the whole
// thing when a request is slow
dg(sn);
} else {
/*
version(cgi_multiple_connections_per_thread) {
bool foundOne = false;
tryAgain:
foreach(t; pool)
if(t.s is null) {
t.s = sn;
foundOne = true;
break;
}
Thread.sleep(dur!"msecs"(1));
if(!foundOne)
goto tryAgain;
} else {
*/
auto thread = new ConnectionThread(sn, &loopBroken, dg);
thread.start();
//}
}
// loopBroken = dg(sn);
} catch(Exception e) {
// if a connection goes wrong, we want to just say no, but try to carry on unless it is an Error of some sort (in which case, we'll die. You might want an external helper program to revive the server when it dies)
sn.close();
}
}
return loopBroken;
}
} }
// helper function to send a lot to a socket. Since this blocks for the buffer (possibly several times), you should probably call it in a separate thread or something. // helper function to send a lot to a socket. Since this blocks for the buffer (possibly several times), you should probably call it in a separate thread or something.
@ -3532,46 +3530,35 @@ class ConnectionException : Exception {
} }
} }
alias int delegate(Socket) CMT; alias void function(Socket) CMT;
import core.thread; import core.thread;
class ConnectionThread : Thread { class ConnectionThread : Thread {
this(Socket s, shared(int)* breakSignifier, CMT dg) { this(ListeningConnectionManager lcm, CMT dg) {
this.s = s; this.lcm = lcm;
this.breakSignifier = breakSignifier;
this.dg = dg; this.dg = dg;
super(&runAll); super(&run);
}
void runAll() {
if(s !is null)
run();
/*
version(cgi_multiple_connections_per_thread) {
while(1) {
while(s is null)
sleep(dur!"msecs"(1));
run();
}
}
*/
} }
void run() { void run() {
scope(exit) { while(true) {
// I don't want to double close it, and it does this on close() according to source lcm.semaphore.wait();
// might be fragile, but meh Socket socket;
if(s.handle() != socket_t.init) synchronized(lcm) {
s.close(); auto idx = lcm.nextIndexFront;
s = null; // so we know this thread is clear socket = lcm.queue[idx];
} lcm.queue[idx] = null;
if(auto result = dg(s)) { atomicOp!"+="(lcm.nextIndexFront, 1);
*breakSignifier = result; atomicOp!"-="(lcm.queueLength, 1);
}
try
dg(socket);
catch(Exception e)
socket.close();
} }
} }
Socket s; ListeningConnectionManager lcm;
shared(int)* breakSignifier;
CMT dg; CMT dg;
} }

28
color.d
View File

@ -6,7 +6,7 @@ module arsd.color;
// importing phobos explodes the size of this code 10x, so not doing it. // importing phobos explodes the size of this code 10x, so not doing it.
private { private {
real toInternal(T)(string s) { real toInternal(T)(scope const(char)[] s) {
real accumulator = 0.0; real accumulator = 0.0;
size_t i = s.length; size_t i = s.length;
foreach(idx, c; s) { foreach(idx, c; s) {
@ -16,8 +16,11 @@ private {
} else if(c == '.') { } else if(c == '.') {
i = idx + 1; i = idx + 1;
break; break;
} else } else {
throw new Exception("bad char to make real from " ~ s); string wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute = "bad char to make real from ";
wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute ~= s;
throw new Exception(wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute);
}
} }
real accumulator2 = 0.0; real accumulator2 = 0.0;
@ -27,8 +30,11 @@ private {
accumulator2 *= 10; accumulator2 *= 10;
accumulator2 += c - '0'; accumulator2 += c - '0';
count *= 10; count *= 10;
} else } else {
throw new Exception("bad char to make real from " ~ s); string wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute = "bad char to make real from ";
wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute ~= s;
throw new Exception(wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute);
}
} }
return accumulator + accumulator2 / count; return accumulator + accumulator2 / count;
@ -80,11 +86,11 @@ private {
return m; return m;
} }
nothrow @safe @nogc pure nothrow @safe @nogc pure
bool startsWithInternal(string a, string b) { bool startsWithInternal(in char[] a, in char[] b) {
return (a.length >= b.length && a[0 .. b.length] == b); return (a.length >= b.length && a[0 .. b.length] == b);
} }
string[] splitInternal(string a, char c) { inout(char)[][] splitInternal(inout(char)[] a, char c) {
string[] ret; inout(char)[][] ret;
size_t previous = 0; size_t previous = 0;
foreach(i, char ch; a) { foreach(i, char ch; a) {
if(ch == c) { if(ch == c) {
@ -97,7 +103,7 @@ private {
return ret; return ret;
} }
nothrow @safe @nogc pure nothrow @safe @nogc pure
string stripInternal(string s) { inout(char)[] stripInternal(inout(char)[] s) {
foreach(i, char c; s) foreach(i, char c; s)
if(c != ' ' && c != '\t' && c != '\n') { if(c != ' ' && c != '\t' && c != '\n') {
s = s[i .. $]; s = s[i .. $];
@ -242,7 +248,7 @@ struct Color {
} }
/// Reads a CSS style string to get the color. Understands #rrggbb, rgba(), hsl(), and rrggbbaa /// Reads a CSS style string to get the color. Understands #rrggbb, rgba(), hsl(), and rrggbbaa
static Color fromString(string s) { static Color fromString(scope const(char)[] s) {
s = s.stripInternal(); s = s.stripInternal();
Color c; Color c;
@ -422,7 +428,7 @@ private string toHexInternal(ubyte b) {
} }
nothrow @safe @nogc pure nothrow @safe @nogc pure
private ubyte fromHexInternal(string s) { private ubyte fromHexInternal(in char[] s) {
int result = 0; int result = 0;
int exp = 1; int exp = 1;

4
dom.d
View File

@ -4819,7 +4819,7 @@ class Table : Element {
tagName = "table"; tagName = "table";
} }
///. /// Creates an element with the given type and content.
Element th(T)(T t) { Element th(T)(T t) {
Element e; Element e;
if(parentDocument !is null) if(parentDocument !is null)
@ -4833,7 +4833,7 @@ class Table : Element {
return e; return e;
} }
///. /// ditto
Element td(T)(T t) { Element td(T)(T t) {
Element e; Element e;
if(parentDocument !is null) if(parentDocument !is null)

View File

@ -226,6 +226,23 @@ final class OpenGlTexture {
/// Make a texture from an image. /// Make a texture from an image.
this(TrueColorImage from) { this(TrueColorImage from) {
bindFrom(from);
}
/// Generates from text. Requires ttf.d
/// pass a pointer to the TtfFont as the first arg (it is template cuz of lazy importing, not because it actually works with different types)
this(T, FONT)(FONT* font, int size, in T[] text) if(is(T == char)) {
bindFrom(font, size, text);
}
/// Creates an empty texture class for you to use with [bindFrom] later
/// Using it when not bound is undefined behavior.
this() {}
/// After you delete it with dispose, you may rebind it to something else with this.
void bindFrom(TrueColorImage from) {
assert(from.width > 0 && from.height > 0); assert(from.width > 0 && from.height > 0);
import core.stdc.stdlib; import core.stdc.stdlib;
@ -295,9 +312,8 @@ final class OpenGlTexture {
free(cast(void*) data); free(cast(void*) data);
} }
/// Generates from text. Requires ttf.d /// ditto
/// pass a pointer to the TtfFont as the first arg (it is template cuz of lazy importing, not because it actually works with different types) void bindFrom(T, FONT)(FONT* font, int size, in T[] text) if(is(T == char)) {
this(T, FONT)(FONT* font, int size, in T[] text) if(is(T == char)) {
assert(font !is null); assert(font !is null);
int width, height; int width, height;
auto data = font.renderString(text, size, width, height); auto data = font.renderString(text, size, width, height);
@ -313,14 +329,26 @@ final class OpenGlTexture {
} }
assert(data.length == 0); assert(data.length == 0);
this(image); bindFrom(image);
}
/// Deletes the texture. Using it after calling this is undefined behavior
void dispose() {
glDeleteTextures(1, &_tex);
_tex = 0;
} }
~this() { ~this() {
glDeleteTextures(1, &_tex); if(_tex > 0)
dispose();
} }
} }
///
void clearOpenGlScreen(SimpleWindow window) {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ACCUM_BUFFER_BIT);
}
// Some math helpers // Some math helpers

View File

@ -197,10 +197,10 @@ class MySqlResult : ResultSet {
+/ +/
class MySql : Database { class MySql : Database {
this(string host, string user, string pass, string db, uint port = 0) { this(string host, string user, string pass, string db, uint port = 0) {
mysql = enforceEx!(DatabaseException)( mysql = enforce!(DatabaseException)(
mysql_init(null), mysql_init(null),
"Couldn't init mysql"); "Couldn't init mysql");
enforceEx!(DatabaseException)( enforce!(DatabaseException)(
mysql_real_connect(mysql, toCstring(host), toCstring(user), toCstring(pass), toCstring(db), port, null, 0), mysql_real_connect(mysql, toCstring(host), toCstring(user), toCstring(pass), toCstring(db), port, null, 0),
error()); error());
@ -371,7 +371,7 @@ class MySql : Database {
override ResultSet queryImpl(string sql, Variant[] args...) { override ResultSet queryImpl(string sql, Variant[] args...) {
sql = escapedVariants(this, sql, args); sql = escapedVariants(this, sql, args);
enforceEx!(DatabaseException)( enforce!(DatabaseException)(
!mysql_query(mysql, toCstring(sql)), !mysql_query(mysql, toCstring(sql)),
error() ~ " :::: " ~ sql); error() ~ " :::: " ~ sql);

View File

@ -9674,6 +9674,9 @@ version(X11) {
scope(exit) XLockDisplay(display); scope(exit) XLockDisplay(display);
win.setSelectionHandler(e); win.setSelectionHandler(e);
} }
break;
case EventType.PropertyNotify:
break; break;
case EventType.SelectionNotify: case EventType.SelectionNotify:
if(auto win = e.xselection.requestor in SimpleWindow.nativeMapping) if(auto win = e.xselection.requestor in SimpleWindow.nativeMapping)
@ -9694,11 +9697,11 @@ version(X11) {
e.xselection.property, e.xselection.property,
0, 0,
100000 /* length */, 100000 /* length */,
false, //false, /* don't erase it */
true, /* do erase it lol */
0 /*AnyPropertyType*/, 0 /*AnyPropertyType*/,
&target, &format, &length, &bytesafter, &value); &target, &format, &length, &bytesafter, &value);
// FIXME: it might be sent in pieces...
// FIXME: I don't have to copy it now since it is in char[] instead of string // FIXME: I don't have to copy it now since it is in char[] instead of string
{ {
@ -9728,15 +9731,51 @@ version(X11) {
} }
} else if(target == GetAtom!"UTF8_STRING"(display) || target == XA_STRING) { } else if(target == GetAtom!"UTF8_STRING"(display) || target == XA_STRING) {
win.getSelectionHandler((cast(char[]) value[0 .. length]).idup); win.getSelectionHandler((cast(char[]) value[0 .. length]).idup);
} else if(target == GetAtom!"INCR"(display)) {
// incremental
//sdpyGettingPaste = true; // FIXME: should prolly be separate for the different selections
// FIXME: handle other events while it goes so app doesn't lock up with big pastes
// can also be optimized if it chunks to the api somehow
char[] s;
do {
XEvent subevent;
do {
XMaskEvent(display, EventMask.PropertyChangeMask, &subevent);
} while(subevent.type != EventType.PropertyNotify || subevent.xproperty.atom != e.xselection.property || subevent.xproperty.state != PropertyNotification.PropertyNewValue);
void* subvalue;
XGetWindowProperty(
e.xselection.display,
e.xselection.requestor,
e.xselection.property,
0,
100000 /* length */,
true, /* erase it to signal we got it and want more */
0 /*AnyPropertyType*/,
&target, &format, &length, &bytesafter, &subvalue);
s ~= (cast(char*) subvalue)[0 .. length];
XFree(subvalue);
} while(length > 0);
win.getSelectionHandler(s);
} else { } else {
// unsupported type // unsupported type
} }
} }
XFree(value); XFree(value);
/*
XDeleteProperty( XDeleteProperty(
e.xselection.display, e.xselection.display,
e.xselection.requestor, e.xselection.requestor,
e.xselection.property); e.xselection.property);
*/
} }
} }
break; break;
@ -10853,6 +10892,8 @@ int XNextEvent(
XEvent* /* event_return */ XEvent* /* event_return */
); );
int XMaskEvent(Display*, arch_long, XEvent*);
Bool XFilterEvent(XEvent *event, Window window); Bool XFilterEvent(XEvent *event, Window window);
int XRefreshKeyboardMapping(XMappingEvent *event_map); int XRefreshKeyboardMapping(XMappingEvent *event_map);

1
ttf.d
View File

@ -92,6 +92,7 @@ struct TtfFont {
stbtt_GetCodepointHMetrics(&font, ch, &advance, &lsb); stbtt_GetCodepointHMetrics(&font, ch, &advance, &lsb);
int cw, cheight; int cw, cheight;
auto c = renderCharacter(ch, size, cw, cheight, x_shift, 0.0); auto c = renderCharacter(ch, size, cw, cheight, x_shift, 0.0);
scope(exit) stbtt_FreeBitmap(c.ptr, null);
int x0, y0, x1, y1; int x0, y0, x1, y1;
stbtt_GetCodepointBitmapBoxSubpixel(&font, ch, scale,scale,x_shift,0, &x0,&y0,&x1,&y1); stbtt_GetCodepointBitmapBoxSubpixel(&font, ch, scale,scale,x_shift,0, &x0,&y0,&x1,&y1);

56
web.d
View File

@ -9,6 +9,11 @@ enum RequirePost;
enum RequireHttps; enum RequireHttps;
enum NoAutomaticForm; enum NoAutomaticForm;
///
struct GenericContainerType {
string type; ///
}
/// Attribute for the default formatting (html, table, json, etc) /// Attribute for the default formatting (html, table, json, etc)
struct DefaultFormat { struct DefaultFormat {
string format; string format;
@ -577,7 +582,7 @@ class ApiProvider : WebDotDBaseType {
/// Returns a list of links to all functions in this class or sub-classes /// Returns a list of links to all functions in this class or sub-classes
/// You can expose it publicly with alias: "alias _sitemap sitemap;" for example. /// You can expose it publicly with alias: "alias _sitemap sitemap;" for example.
Element _sitemap() { Element _sitemap() {
auto container = _getGenericContainer(); auto container = Element.make("div", "", "sitemap");
void writeFunctions(Element list, in ReflectionInfo* reflection, string base) { void writeFunctions(Element list, in ReflectionInfo* reflection, string base) {
string[string] handled; string[string] handled;
@ -617,7 +622,7 @@ class ApiProvider : WebDotDBaseType {
starting = cgi.logicalScriptName ~ cgi.pathInfo; // FIXME starting = cgi.logicalScriptName ~ cgi.pathInfo; // FIXME
writeFunctions(list, reflection, starting ~ "/"); writeFunctions(list, reflection, starting ~ "/");
return list.parentNode.removeChild(list); return container;
} }
/// If the user goes to your program without specifying a path, this function is called. /// If the user goes to your program without specifying a path, this function is called.
@ -626,13 +631,18 @@ class ApiProvider : WebDotDBaseType {
throw new Exception("no default"); throw new Exception("no default");
} }
/// forwards to [_getGenericContainer]("default")
Element _getGenericContainer() {
return _getGenericContainer("default");
}
/// When the html document envelope is used, this function is used to get a html element /// When the html document envelope is used, this function is used to get a html element
/// where the return value is appended. /// where the return value is appended.
/// It's the main function to override to provide custom HTML templates. /// It's the main function to override to provide custom HTML templates.
/// ///
/// The default document provides a default stylesheet, our default javascript, and some timezone cookie handling (which you must handle on the server. Eventually I'll open source my date-time helpers that do this, but the basic idea is it sends an hour offset, and you can add that to any UTC time you have to get a local time). /// The default document provides a default stylesheet, our default javascript, and some timezone cookie handling (which you must handle on the server. Eventually I'll open source my date-time helpers that do this, but the basic idea is it sends an hour offset, and you can add that to any UTC time you have to get a local time).
Element _getGenericContainer() Element _getGenericContainer(string containerName)
out(ret) { out(ret) {
assert(ret !is null); assert(ret !is null);
} }
@ -766,7 +776,7 @@ struct ReflectionInfo {
// these might go away. // these might go away.
string defaultOutputFormat = "html"; string defaultOutputFormat = "default";
int versionOfOutputFormat = 2; // change this in your constructor if you still need the (deprecated) old behavior int versionOfOutputFormat = 2; // change this in your constructor if you still need the (deprecated) old behavior
// bool apiMode = false; // no longer used - if format is json, apiMode behavior is assumed. if format is html, it is not. // bool apiMode = false; // no longer used - if format is json, apiMode behavior is assumed. if format is html, it is not.
// FIXME: what if you want the data formatted server side, but still in a json envelope? // FIXME: what if you want the data formatted server side, but still in a json envelope?
@ -815,6 +825,8 @@ struct FunctionInfo {
bool requireHttps; bool requireHttps;
string genericContainerType = "default";
Document delegate(in string[string] args) createForm; /// This is used if you want a custom form - normally, on insufficient parameters, an automatic form is created. But if there's a functionName_Form method, it is used instead. FIXME: this used to work but not sure if it still does Document delegate(in string[string] args) createForm; /// This is used if you want a custom form - normally, on insufficient parameters, an automatic form is created. But if there's a functionName_Form method, it is used instead. FIXME: this used to work but not sure if it still does
} }
@ -1000,6 +1012,8 @@ immutable(ReflectionInfo*) prepareReflectionImpl(alias PM, alias Parent)(Parent
f.returnType = ReturnType!(__traits(getMember, Class, member)).stringof; f.returnType = ReturnType!(__traits(getMember, Class, member)).stringof;
f.returnTypeIsDocument = is(ReturnType!(__traits(getMember, Class, member)) : Document); f.returnTypeIsDocument = is(ReturnType!(__traits(getMember, Class, member)) : Document);
f.returnTypeIsElement = is(ReturnType!(__traits(getMember, Class, member)) : Element); f.returnTypeIsElement = is(ReturnType!(__traits(getMember, Class, member)) : Element);
static if(hasValueAnnotation!(__traits(getMember, Class, member), GenericContainerType))
f.genericContainerType = getAnnotation!(__traits(getMember, Class, member), GenericContainerType).type;
f.parentObject = reflection; f.parentObject = reflection;
@ -1573,9 +1587,9 @@ void run(Provider)(Cgi cgi, Provider instantiation, size_t pathInfoStartingPoint
Element e; Element e;
auto hack = cast(ApiProvider) realObject; auto hack = cast(ApiProvider) realObject;
if(hack !is null) if(hack !is null)
e = hack._getGenericContainer(); e = hack._getGenericContainer(fun is null ? "default" : fun.genericContainerType);
else else
e = instantiation._getGenericContainer(); e = instantiation._getGenericContainer(fun is null ? "default" : fun.genericContainerType);
document = e.parentDocument; document = e.parentDocument;
@ -1881,6 +1895,7 @@ Form createAutomaticForm(Document document, string action, in Parameter[] parame
auto fmt = Element.make("select"); auto fmt = Element.make("select");
fmt.name = "format"; fmt.name = "format";
fmt.addChild("option", "Automatic").setAttribute("value", "default");
fmt.addChild("option", "html").setAttribute("value", "html"); fmt.addChild("option", "html").setAttribute("value", "html");
fmt.addChild("option", "table").setAttribute("value", "table"); fmt.addChild("option", "table").setAttribute("value", "table");
fmt.addChild("option", "json").setAttribute("value", "json"); fmt.addChild("option", "json").setAttribute("value", "json");
@ -2741,6 +2756,15 @@ WrapperFunction generateWrapper(alias ObjectType, string funName, alias f, R)(Re
// FIXME: it's awkward to call manually due to the JSONValue ref thing. Returning a string would be mega nice. // FIXME: it's awkward to call manually due to the JSONValue ref thing. Returning a string would be mega nice.
string formatAs(T, R)(T ret, string format, R api = null, JSONValue* returnValue = null, string formatJsonToStringAs = null) if(is(R : ApiProvider)) { string formatAs(T, R)(T ret, string format, R api = null, JSONValue* returnValue = null, string formatJsonToStringAs = null) if(is(R : ApiProvider)) {
if(format == "default") {
static if(is(typeof(ret) : K[N][V], size_t N, K, V)) {
format = "table";
} else {
format = "html";
}
}
string retstr; string retstr;
if(api !is null) { if(api !is null) {
static if(__traits(compiles, api._customFormat(ret, format))) { static if(__traits(compiles, api._customFormat(ret, format))) {
@ -2803,8 +2827,18 @@ string formatAs(T, R)(T ret, string format, R api = null, JSONValue* returnValue
goto badType; goto badType;
gotATable(table); gotATable(table);
break; break;
} } else static if(is(typeof(ret) : K[N][V], size_t N, K, V)) {
else auto table = cast(Table) Element.make("table");
table.addClass("data-display");
foreach(k, v; ret) {
auto row = table.addChild("tr");
foreach(cell; v)
table.addChild("td", to!string(cell));
}
gotATable(table);
break;
} else
goto badType; goto badType;
default: default:
badType: badType:
@ -3899,7 +3933,7 @@ bool checkPassword(string saltedPasswordHash, string userSuppliedPassword) {
/// implements the "table" format option. Works on structs and associative arrays (string[string][]) /// implements the "table" format option. Works on structs and associative arrays (string[string][])
Table structToTable(T)(Document document, T arr, string[] fieldsToSkip = null) if(isArray!(T) && !isAssociativeArray!(T)) { Table structToTable(T)(Document document, T arr, string[] fieldsToSkip = null) if(isArray!(T) && !isAssociativeArray!(T)) {
auto t = cast(Table) document.createElement("table"); auto t = cast(Table) document.createElement("table");
t.border = "1"; t.attrs.border = "1";
static if(is(T == string[string][])) { static if(is(T == string[string][])) {
string[string] allKeys; string[string] allKeys;
@ -3929,7 +3963,7 @@ Table structToTable(T)(Document document, T arr, string[] fieldsToSkip = null) i
odd = !odd; odd = !odd;
} }
} else static if(is(typeof(T[0]) == struct)) { } else static if(is(typeof(arr[0]) == struct)) {
{ {
auto thead = t.addChild("thead"); auto thead = t.addChild("thead");
auto tr = thead.addChild("tr"); auto tr = thead.addChild("tr");
@ -3951,7 +3985,7 @@ Table structToTable(T)(Document document, T arr, string[] fieldsToSkip = null) i
odd = !odd; odd = !odd;
} }
} else static assert(0); } else static assert(0, T.stringof);
return t; return t;
} }