Skip to content

Commit

Permalink
Merge pull request #106 from eskimor/form_handler
Browse files Browse the repository at this point in the history
Implemented registerFormInterface.
  • Loading branch information
s-ludwig committed Oct 23, 2012
2 parents ddf1adf + 8dc7961 commit 1094416
Show file tree
Hide file tree
Showing 7 changed files with 367 additions and 35 deletions.
7 changes: 7 additions & 0 deletions examples/form_interface/package.json
@@ -0,0 +1,7 @@
{
"name": "form-interface-example",
"description": "Uses the registerFormInterface function for making functions available to JavaScript and to Diet templates.",
"authors": [ "Robert Klotzner" ],
"version": "0.0.1",
"dependencies": {}
}
92 changes: 92 additions & 0 deletions examples/form_interface/source/app.d
@@ -0,0 +1,92 @@
import vibe.d;
import std.stdio;
import std.algorithm;
import std.string;
import vibe.http.form;
/**
This example pretty well shows how registerFormInterface is meant to be used and what possibilities it offers.
The API that is exposed by DataProvider is conveniently usable by the methods in App and from JavaScript, with just
one line of code:
---
registerFormInterface(router, provider_, prefix~"dataProvider/");
---
The tableview.dt uses JavaScript for just replacing the table if filter-data changes if no JavaScript is available
the whole site gets reloaded.
*/

/**
* This class serves its users array as html table. Also have a look at views/createTable.dt.
*/
class DataProvider {
enum Fields {
nameid, surnameid, addressid
}
void index(HttpServerRequest req, HttpServerResponse res) {
getData(req, res);
}
void getData(HttpServerRequest req, HttpServerResponse res) {
auto table=users;
res.render!("createTable.dt", table)();
}
/**
Overload that takes an enumeration for indexing the users array in a secure way and a value to filter on.
Method code does not have to care about validating user data, no need to check that a field is actually present, no manual conversion, ...
Say that this is not ingenious, I love D.
*/
void getData(HttpServerRequest req, HttpServerResponse res, Fields field, string value) {
auto table=users.filter!((a) => value.length==0 || a[field]==value)();
res.render!("createTable.dt", table)();
}
/// Add a new user to the array, using this method from JavaScript is left as an exercise.
void addUser(string name, string surname, string address) {
users~=[name, surname, address];
}
private:
string[][] users=[["Tina", "Muster", "Wassergasse 12"],
["Martina", "Maier", "Broadway 6"],
["John", "Foo", "Church Street 7"]];
}

class App {
this(UrlRouter router, string prefix="/") {
provider_=new DataProvider;
prefix_= prefix.length==0 || prefix[$-1]!='/' ? prefix~"/" : prefix;
registerFormInterface(router, this, prefix);
registerFormInterface(router, provider_, prefix~"dataProvider/");
}
void index(HttpServerRequest req, HttpServerResponse res) {
getTable(req, res);
}
void getTable(HttpServerRequest req, HttpServerResponse res) {
res.headers["Content-Type"] = "text/html";
res.render!("tableview.dt", req, res, dataProvider)();
}
void getTable(HttpServerRequest req, HttpServerResponse res, DataProvider.Fields field, string value) {
res.headers["Content-Type"] = "text/html";
res.render!("tableview.dt", req, res, dataProvider, field, value)();
}
void addUser(HttpServerRequest req, HttpServerResponse res, string name, string surname, string address) {
dataProvider.addUser(name, surname, address);
res.redirect(prefix);
}
@property string prefix() {
return prefix_;
}
@property DataProvider dataProvider() {
return provider_;
}
private:
string prefix_;
DataProvider provider_;
}

static this()
{
auto settings = new HttpServerSettings;
settings.port = 8080;
auto router = new UrlRouter;
auto app=new App(router);
listenHttp(settings, router);
}
5 changes: 5 additions & 0 deletions examples/form_interface/views/createTable.dt
@@ -0,0 +1,5 @@
table
- foreach(row; table)
tr
- foreach(column; row)
td #{column}
12 changes: 12 additions & 0 deletions examples/form_interface/views/layout.dt
@@ -0,0 +1,12 @@
!!! 5
html(lang="en")
head
block head
body
block body
.content
block content
.sidebar
block sidebar
.footer
block footer
51 changes: 51 additions & 0 deletions examples/form_interface/views/tableview.dt
@@ -0,0 +1,51 @@
extends layout
prepend block head
title ="Welcome!"
block content
.clear
h1 Welcome!
.datatable(id="datatable")
h2 Users
- static if(__traits(compiles, value))
- dataProvider.getData(req, res, field, value);
- else
- dataProvider.getData(req, res);
.filterbycontainer
h2 Filter users by field value
form(onsubmit="return getTable(this.field.value, this.value.value)", action='/getTable', method='GET')
table
tr
td Filter by:
td
select(name="field")
option(value="nameid") Name
option(value="surnameid") Surname
option(value="addressid") Address
tr
td Value:
td
input(type="text", width="160", name="value")

.additemcontainer
h2 Add new user
form(action='/addUser', method='POST')
table
- import std.string;
- foreach ( item ; ["name", "surname", "address"] )
tr
td= item[0..1].toUpper()~item[1..$]
td
input(type="text", width="160", name="#{item}")
tr
td
td
input(type='submit', value="Add")
script
function getTable(field, value) {
var req = new XMLHttpRequest();
req.open('GET', '/dataProvider/getData?field='+field+'&value='+value, false);
req.send();
var tablediv=document.getElementById('datatable');
tablediv.innerHTML = req.responseText;
return false;
}
200 changes: 200 additions & 0 deletions source/vibe/http/form.d
Expand Up @@ -14,10 +14,19 @@ import vibe.inet.message;
import vibe.inet.url;
import vibe.textfilter.urlencode;

// needed for registerFormInterface stuff:
import vibe.http.rest;
import vibe.http.server;
import vibe.http.router;


import std.array;
import std.exception;
import std.string;

// needed for registerFormInterface stuff:
import std.traits;
import std.conv;

struct FilePart {
InetHeaderMap headers;
Expand Down Expand Up @@ -137,3 +146,194 @@ private bool parseMultipartFormPart(InputStream stream, ref string[string] form,
return stream.readLine(max_line_length) != "--";
}


/**
Generates a form based interface to the given instance.
Each function is callable with either GET or POST using form encoded
parameters. All methods of I that start with "get", "query", "add", "create",
"post" are made available via url: url_prefix~method_name. A method named
"index" will be made available via url_prefix. All these methods might take a
HttpServerRequest parameter and a HttpServerResponse parameter, but don't have
to.
All additional parameters will be filled with available form-data fields.
Every parameters name has to match a form field name. The registered handler
will throw an exception if no overload is found that is compatible with all
available form data fields.
For a thorough example of how to use this method, see the form_interface
example in the examples directory.
See_Also: registerFormMethod
Params:
router = The router the found methods are registered with.
instance = The instance whose methods should be called via the registered URLs.
url_prefix = The prefix before the method name. A method named getWelcomePage
with a given url_prefix="/mywebapp/welcomePage/" would be made available as
"/mywebapp/welcomePage/getWelcomePage" if MethodStyle is Unaltered.
style = How the url part representing the method name should be altered.
*/
void registerFormInterface(I)(UrlRouter router, I instance, string url_prefix,
MethodStyle style = MethodStyle.Unaltered)
{
foreach( method; __traits(allMembers, I) ){

static if( method.startsWith("get") || method.startsWith("query") || method.startsWith("add")
|| method.startsWith("create") || method.startsWith("post") || method == "index" ) {
registerFormMethod!method(router, instance, url_prefix, style);
}
}
}
/**
Registers just a single method.
For details see registerFormInterface. This method does exactly the
same, but instead of registering found methods that match a scheme it just
registers the method specified. See_Also: registerFormInterface
Params:
method = The name of the method to register. It might be
overloaded, one overload has to match any given form data, otherwise an error is triggered.
*/
void registerFormMethod(string method, I)(UrlRouter router, I instance, string url_prefix, MethodStyle style = MethodStyle.Unaltered)
{
string url(string name) {
return url_prefix ~ adjustMethodStyle(name, style);
}

auto handler=formMethodHandler!(I, method)(instance);
string url_method= method=="index" ? "" : method;
router.get(url(url_method), handler);
router.post(url(url_method), handler);
}


/**
Generate a HttpServerRequestDelegate from a generic function with arbitrary arguments.
The arbitrary arguments will be filled in with data from the form in req. For details see applyParametersFromAssociativeArrays.
See_Also: applyParametersFromAssociativeArrays
Params:
delegate = Some function, which some arguments which must be constructible from strings with to!ArgType(some_string), except one optional parameter
of type HttpServerRequest and one of type HttpServerResponse which are passed over.
Returns: A HttpServerRequestDelegate which passes over any form data to the given function.
*/
/// This is private because untested and I am also not sure whether it a) works and b) if it is useful at all.
/// private
HttpServerRequestDelegate formMethodHandler(DelegateType)(DelegateType func) if(isCallable!DelegateType)
{
void handler(HttpServerRequest req, HttpServerResponse res)
{
string error;
enforce(applyParametersFromAssociativeArray(req, res, func, error), error);
}
return &handler;
}

/**
Create a delegate handling form data for any matching overload of T.method.
T is some class or struct. Method some probably overloaded method of T. The returned delegate will try all overloads
of the passed method and will only raise an error if no conforming overload is found.
*/
HttpServerRequestDelegate formMethodHandler(T, string method)(T inst)
{
import std.stdio;
void handler(HttpServerRequest req, HttpServerResponse res)
{
import std.traits;
string[string] form = req.method == HttpMethod.GET ? req.query : req.form;
// alias MemberFunctionsTuple!(T, method) overloads;
string errors;
foreach(func; __traits(getOverloads, T, method)) {
string error;
ReturnType!func delegate(ParameterTypeTuple!func) myoverload=&__traits(getMember, inst, method);
if(applyParametersFromAssociativeArray!func(req, res, myoverload, error)) {
return;
}
errors~="Overload "~method~typeid(ParameterTypeTuple!func).toString()~" failed: "~error~"\n";
}
enforce(false, "No method found that matches the found form data:\n"~errors);
}
return &handler;
}

/**
Tries to apply all named arguments in args to func.
If it succeeds it calls the function with req, res (if it has one
parameter of type HttpServerRequest and one of type HttpServerResponse), and
all the values found in args.
If any supplied argument could not be applied or the method has
requires more arguments than given, the method returns false and does not call
func. In this case error gets filled with some string describing which
parameters could not be applied. Exceptions are not used in this situation,
because when traversing overloads this might be a quite common scenario.
Calls: applyParametersFromAssociativeArray!(Func,Func)(req, res, func, error),
if you want to handle overloads of func, use the second version of this method
and pass the overload alias as first template parameter. (For retrieving parameter names)
See_Also: formMethodHandler
Params:
req = The HttpServerRequest object that gets queried for form
data (req.query for GET requests, req.form for POST requests) and that is
passed on to func, if func has a parameter of matching type. Each key in the
form data must match a parameter name, the corresponding value is then applied.
HttpServerRequest and HttpServerResponse arguments are excluded as they are
qrovided by the passed req and res objects.
res = The response object that gets passed on to func if func
has a parameter of matching type.
error = This string will be set to a descriptive message if not all parameters could be matched.
Returns: true if successful, false otherwise.
*/
/// private
private bool applyParametersFromAssociativeArray(Func)(HttpServerRequest req, HttpServerResponse res, Func func, out string error) {
return applyParametersFromAssociativeArray!(Func, Func)(req, res, func, error);
}
/// Overload which takes additional parameter for handling overloads of func.
/// private
private bool applyParametersFromAssociativeArray(alias Overload, Func)(HttpServerRequest req, HttpServerResponse res, Func func, out string error) {
alias ParameterTypeTuple!Overload ParameterTypes;
ParameterTypes args;
string[string] form = req.method == HttpMethod.GET ? req.query : req.form;
int count=0;
foreach(i, item; args) {
static if(is(ParameterTypes[i] : HttpServerRequest)) {
args[i] = req;
}
else static if(is(ParameterTypes[i] : HttpServerResponse)) {
args[i] = res;
}
else {
count++;
}
}
if(count!=form.length) {
error="The form had "~to!string(form.length)~" element(s), but "~to!string(count)~" parameter(s) need to be supplied.";
return false;
}
foreach(i, item; ParameterIdentifierTuple!Overload) {
static if(!is( typeof(args[i]) : HttpServerRequest) && !is( typeof(args[i]) : HttpServerResponse)) {
if(item !in form) {
error="Form misses parameter: "~item;
return false;
}
args[i] = to!(typeof(args[i]))(form[item]);
}
}
func(args);
return true;
}

0 comments on commit 1094416

Please sign in to comment.