Router¶
Router is the core component of Valum. It dispatches request to the right handler and processes certain error conditions described in Redirection and Error.
The router is constituted of a sequence of Route
objects which may or may
not match incoming requests and perform the process described in their
handlers.
Route¶
The most basic and explicit way of attaching a handler is Router.route
,
which attach the provided Route
object to the sequence.
app.route (new RuleRoute (Method.GET, "/", null, () => {}));
Route are simple objects which combine a matching and handling processes. The
following sections implicitly treat of route objects such such as RuleRoute
and RegexRoute
.
Method¶
New in version 0.3.
The Method
flag provide a list of HTTP methods and some useful masks used
into route definitions.
Flag | Description |
---|---|
Method.SAFE |
safe methods |
Method.IDEMPOTENT |
idempotent methods (e.g. SAFE and PUT ) |
Method.CACHEABLE |
cacheable methods (e.g. HEAD , GET and POST ) |
Method.ALL |
all standard HTTP methods |
Method.OTHER |
any non-standard HTTP methods |
Method.ANY |
anything, including non-standard methods |
Method.PROVIDED |
indicate that the route provide its methods |
Method.META |
mask for all meta flags like Method.PROVIDED |
Note
Safe, idempotent and cacheable methods are defined in section 4.2 of RFC 7231.
Using a flag makes it really convenient to capture multiple methods with the
|
binary operator.
app.rule (Method.GET | Method.POST, "/", (req, res) => {
// matches GET and POST
});
Method.GET
is defined as Method.ONLY_GET | Method.HEAD
such that
defining the former will also provide a HEAD
implementation. In general,
it’s recommended to check the method in order to skip a body that won’t be
considered by the user agent.
app.get ("/", () => {
res.headers.set_content_type ("text/plain", null);
if (req.method == Request.HEAD) {
return res.end (); // skip unnecessary I/O
}
return res.expand_utf8 ("Hello world!");
});
To provide only the GET
part, use Method.ONLY_GET
.
app.rule (Method.ONLY_GET, "/", () => {
res.headers.set_content_type ("text/plain", null);
return res.expand_utf8 ("Hello world!");
});
Per definition, POST
is considered cacheable, but if it’s not desirable, it
may be removed from the mask with the unary ~
operator.
app.rule (Method.CACHEABLE & ~Method.POST, "/", () => {
res.headers.set_content_type ("text/plain", null);
return res.expand_utf8 ("Hello world!");
});
Non-standard method¶
To handle non-standard HTTP method, use the Method.OTHER
along with an
explicit check.
app.method (Method.OTHER, "/rule", (req, res) => {
if (req.method != "CUSTOM")
return next ();
});
Reverse¶
New in version 0.3.
Some route implementations can be reversed into URLs by calling
Route.to_url
or the alternative Route.to_urlv
and
Route.to_url_from_hash
. It may optionally take parameters which, in the
case of the rule-based route, correspond to the named captures.
Introspection¶
The router introspect the route sequence to determine what methods are allowed
for a given URI and thus produce a nice Allow
header. To mark a method as
provided, the Method.PROVIDED
flag has to be used. This is automatically
done for the helpers and the Router.rule
function described below.
Additionally, the OPTIONS
and TRACE
are automatically handled if not
specified for a path. The OPTIONS
will produce a Allow
header and
TRACE
will feedback the request into the response payload.
Named route¶
New in version 0.3.
Few of the helpers provided by the router also accept an additional parameter
to name the created route object. This can then be used to generate reverse
URLs with Router.url_for
.
Note
This feature is only support for the rule-based and path-based route implementations.
var app = new Router ();
app.get ("/", (req, res) => {
return res.expand_utf8 ("Hello world! %s".printf (app.url_for ("home")));
}, "home");
Likewise to to_url
, it’s possible to pass additional parameters as varidic
arguments. The following example show how one can serve relocatable static
resources and generate URLs in a Compose template.
using Compose.HTML5;
using Valum;
using Valum.Static;
var app = new Router ();
app.get ("/", () => {
return res.expand_utf8 (
html (
head (
title ("Hello world!"),
link ("stylesheet",
app.url_for ("static",
"path", "bootstrap/dist/css/bootstrap.min.css"))),
body ()));
});
app.get ("/static/<path>", serve_from_path ("static"), "static");
Other helpers are provided to pass a GLib.HashTable
via Router.url_for_hash
or explicit varidic arguments via Router.url_for_valist
.
Use¶
New in version 0.3.
The simplest way to attach a handler is Router.use
, which unconditionally
apply the route on the request.
app.use ((req, res, next) => {
var params = new HashTable<string, string> (str_hash, str_equal);
params["charset"] = "iso-8859-1";
res.headers.set_content_type ("text/xhtml+xml", params);
return next ();
});
It is typically used to mount a Middlewares on the router.
Asterisk¶
New in version 0.3.
The special *
URI is handled by the Router.asterisk
helper. It is
typically used along with the OPTIONS
method to provide a self-description
of the web service or application.
app.asterisk (Method.OPTIONS, () => {
return true;
});
Rule¶
Changed in version 0.3: Rule helpers (e.g. get
, post
, rule
) must explicitly be provided
with a leading slash.
The rule syntax has been greatly improved to support groups, optionals and wildcards.
The de facto way of attaching handler callbacks is based on the rule system.
The Router.rule
as well as all HTTP method helpers use it.
app.rule (Method.ALL, "/rule" (req, res) => {
return true;
});
The syntax for rules is given by the following EBNF grammar:
rule = piece | parameter | group | optional | wildcard, [ rule ];
group = '(', rule, ')';
optional = (piece | parameter | group), '?';
wildcard = '*';
parameter = '<', [ type, ':' ], name, '>'; (* considered as a terminal *)
type = ? any sequence of word character ?;
name = ? any sequence of word character ?;
piece = ? any sequence of URL-encoded character ?;
Remarks¶
- a piece is a single character, so
/users/?
only indicates that the/
is optional - the wildcard
*
matches anything, just like the.*
regular expression
The following table show valid rules and their corresponding regular expressions. Note that rules are matching the whole path as they are automatically anchored.
Rule | Regular expression |
---|---|
/user |
^/user$ |
/user/<id> |
^/user/(?<id>\w+)$ |
/user/<int:id> |
^/user/(?<id>\d+)$ |
/user(/<int:id>)? |
^/user(?:/(?<id>\d+))?$ |
Types¶
Valum provides built-in types initialized in the Router constructor. The following table details these types and what they match.
Type | Regex | Description |
---|---|---|
int |
\d+ |
matches non-negative integers like a database primary key |
string |
\w+ |
matches any word character |
path |
(?:\.?[\w/-\s/])+ |
matches a piece of route including
slashes, but not .. |
Undeclared types default to string
, which matches any word characters.
It is possible to specify or overwrite types using the types
map in
Router. This example will define the path
type matching words and
slashes using a regular expression literal.
app.register_type ("path", new Regex ("[\w/]+", RegexCompileFlags.OPTIMIZE));
If you would like ìnt
to match negatives integer, you may just do:
app.register_type ("int", new Regex ("-?\d+", RegexCompileFlags.OPTIMIZE));
Rule parameters are available from the routing context by their name.
app.get ("/<controller>/<action>", (req, res, next, context) => {
var controller = context["controller"].get_string ();
var action = context["action"].get_string ();
});
Helpers¶
Helpers for the methods defined in the HTTP/1.1 protocol and the extra
TRACE
methods are included. The path is matched according to the rule
system defined previously.
app.get ("/", (req, res) => {
return res.expand_utf8 ("Hello world!");
});
The following example deal with a POST
request providing using Soup.Form
to decode the payload.
app.post ("/login", (req, res) => {
var data = Soup.Form.decode (req.flatten_utf8 ());
var username = data["username"];
var password = data["password"];
// assuming you have a session implementation in your app
var session = new Session.authenticated_by (username, password);
return true;
});
Regular expression¶
Changed in version 0.3: The regex helper must be provided with an explicit leading slash.
If the rule system does not suit your needs, it is always possible to use regular expression. Regular expression will be automatically scoped, anchored and optimized.
app.regex (Method.GET, new Regex ("/home/?", RegexCompileFlags.OPTIMIZE), (req, res) => {
return res.body.write_all ("Matched using a regular expression.".data, true);
});
Named captures are registered on the routing context.
app.regex (new Regex ("/(?<word>\w+)", RegexCompileFlags.OPTIMIZE), (req, res, next, ctx) => {
var word = ctx["word"].get_string ();
});
Matcher callback¶
Request can be matched by a simple callback typed by the MatcherCallback
delegate.
app.matcher (Method.GET, (req) => { return req.uri.get_path () == "/home"; }, (req, res) => {
// matches /home
});
Scoping¶
Changed in version 0.3: The scope feature does not include a slash, instead you should scope with a leading slash like shown in the following examples.
Scoping is a powerful prefixing mechanism for rules and regular expressions.
Route declarations within a scope will be prefixed by <scope>
.
The Router
maintains a scope stack so that when the program flow enter
a scope, it pushes the fragment on top of that stack and pops it when it exits.
app.scope ("/admin", (admin) => {
// admin is a scoped Router
app.get ("/users", (req, res) => {
// matches /admin/users
});
});
app.get ("/users", (req, res) => {
// matches /users
});
To literally mount an application on a prefix, see the Basepath middleware.
Context¶
New in version 0.3.
During the routing, states can obtained from a previous handler or passed to the next one using the routing context.
Keys are resolved recursively in the tree of context by looking at the parent context if it’s missing.
app.get ("/", (req, res, next, context) => {
context["some key"] = "some value";
return next ();
});
app.get ("/", (req, res, next, context) => {
var some_value = context["some key"]; // or context.parent["some key"]
return return res.body.write_all (some_value.data, null);
});
Next¶
Changed in version 0.3: The next
continuation does not take the request and response objects as
parameter. To perform transformation, see Converters and
Middlewares.
The handler takes a callback as an optional third argument. This callback is a continuation that will continue the routing process to the next matching route.
app.get ("/", (req, res, next) => {
return next (); // keep routing
});
app.get ("/", (req, res) => {
// this is invoked!
});
Error handling¶
New in version 0.2.1: Prior to this release, any unhandled error would crash the main loop iteration.
Changed in version 0.3: Error and status codes are now handled with a catch
block or using the
Status middleware.
Changed in version 0.3: The default handling is not ensured by the Basic middleware.
Changed in version 0.3: Thrown errors are forwarded to VSGI, which process them essentially the same way. See VSGI for more details.
Similarly to status codes, errors are propagated in the HandlerCallback
and
NextCallback
delegate signatures and can be handled in a catch
block.
app.use (() => {
try {
return next ();
} catch (IOError err) {
res.status = 500;
return res.expand_utf8 (err.message);
}
});
app.get ("/", (req, res) => {
throw new IOError.FAILED ("I/O failed some some reason.");
});
Thrown status code can also be caught this way, but it’s much more convenient to use the Status middleware.
Subrouting¶
Since VSGI.ApplicationCallback
is type compatible with HandlerCallback
,
it is possible to delegate request handling to another VSGI-compliant
application.
In particular, it is possible to treat Router.handle
like any handling
callback.
Note
This feature is a key design of the router and is intended to be used for a maximum inter-operability with other frameworks based on VSGI.
The following example delegates all GET
requests to another router which
will process in isolation with its own routing context.
var app = new Router ();
var api = new Router ();
// delegate all GET requests to api router
app.get ("*", api.handle);
One common pattern with subrouting is to attempt another router and fallback on
next
.
var app = new Router ();
var api = new Router ();
app.get ("/some-resource", (req, res) => {
return api.handle (req, res) || next ();
});
Cleaning up route logic¶
Performing a lot of route bindings can get messy, particularly if you want to
split an application several reusable modules. Encapsulation can be achieved by
subclassing Router
and performing initialization in a construct
block:
public class AdminRouter : Router {
construct {
rule (Method.GET, "/admin/user", view);
rule (Method.GET | Method.POST, "/admin/user/<int:id>", edit);
}
public bool view (Request req, Response res) {
return render_template ("users", Users.all ());
}
public bool edit (Request req, Response res) {
var user = User.find (ctx["id"]);
if (req.method == "POST") {
user.values (Soup.Form.decode (req.flatten_utf8 ()));
user.update ();
}
return render_template ("user", user);
}
}
Using subrouting, it can be assembled to a parent router given a rule (or any
matching process described in this document). This way, incoming request having
the /admin/
path prefix will be delegated to the admin
router.
var app = new Router ();
app.rule (Method.ALL, "/admin/*", new AdminRouter ().handle);
The Basepath middleware provide very handy path isolation so
that the router can be simply written upon the leading /
and rebased on any
basepath. In that case, we can strip the leading /admin
in router’s rules.
var app = new Router ();
// captures '/admin/users' and '/admin/user/<int:id>'
app.use (basepath ("/admin", new AdminRouter ().handle));