action-pack ~master
A router and controller implemenation for use with vibe.d
To use this package, run the following command in your project's root directory:
Manual usage
Put the following dependency into your project's dependences section:
D Lang Action Pack
A router for use with Vibe-D that mimics the way routers and controllers work in Rails.
Installation
Add the following to your package.json
file:
"dependencies": {
"action-pack": "~master"
}
Usage
Here's a very quick sample run through of how you can use the ActionPack
routing.
App.d
and the router
First you have to setup your app.d
file with your router and your list of routes:
// app.d
import vibe.d;
import action_dispatch.all;
shared static this()
{
auto router = new ActionRouter;
router.get("/", "BooksController", "index");
router.resources("authors", delegate void (ActionNamespace authors) {
authors.resources("books");
});
auto settings = new HTTPServerSettings;
settings.port = 8080;
listenHTTP(settings, router);
}
In the above example we map the default route of /
to be handled by the BooksController
action index
.
Then we define a resource, authors
and a sub resource for authors
: books
. This generates a routing table:
Method | Route | Controller | Action |
---|---|---|---|
GET | / | BooksController | #ndex |
GET | /authors/:author_id/books | BooksController | #index |
GET | /authors/:author_id/books/new | BooksController | #init |
POST | /authors/:author_id/books | BooksController | #create |
GET | /authors/:author_id/books/:id | BooksController | #show |
GET | /authors/:author_id/books/:id/edit | BooksController | #edit |
PUT | /authors/:author_id/books/:id | BooksController | #update |
PATCH | /authors/:author_id/books/:id | BooksController | #update |
DELETE | /authors/:author_id/books/:id | BooksController | #destroy |
GET | /authors | AuthorsController | #index |
GET | /authors/new | AuthorsController | #init |
POST | /authors | AuthorsController | #create |
GET | /authors/:id | AuthorsController | #show |
GET | /authors/:id/edit | AuthorsController | #edit |
PUT | /authors/:id | AuthorsController | #update |
PATCH | /authors/:id | AuthorsController | #update |
DELETE | /authors/:id | AuthorsController | #destroy |
From just a few lines of code we get 17 routes! All with automatic params. As you can see, we made a ton of assumptinos to get here, but this way favors convention over a bunch of manual configuration.
Controllers
Next we setup our two controller:
// /source/core/controllers/authors_controller.d
module core.controllers.authors;
import action_dispatch.all;
class AuthorsController : ActionController {
mixin DynamicClassImplementation!();
@DynamicallyAvailable {
// GET /authors
void index() {
respondTo(delegate void(Format format) {
format.html(delegate void() {
response.render!("books/index.dt", request);
});
format.json(delegate void() { // render some JSON });
});
}
// GET /authors/new
void init() {
// render the right view
}
// POST /authors
void create() {
// render the right view
}
// GET /authors/:id
void show() {
// render the right view
}
// GET /authors/:id/edit
void edit() {
// render the right view
}
// PUT/PATCH /authors/:id
void update() {
// render the right view
}
// DELETE /authors/:id
void destroy() {
// render the right view
}
}
}
// /source/core/controllers/books_controller.d
module core.controllers.books;
import action_dispatch.all;
class BooksController : ActionController {
mixin DynamicClassImplementation!();
@DynamicallyAvailable {
// GET /authors/:authors_id/books
void index() {
respondTo(delegate void(Format format) {
format.html(delegate void() {
response.render!("books/index.dt", request);
});
format.json(delegate void() { // render some JSON });
});
}
// GET /authors/:authors_id/books/new
void init() {
// render the right view
}
// POST /authors/:authors_id/books
void create() {
// render the right view
}
// GET /authors/:authors_id/books/:id
void show() {
// render the right view
}
// GET /authors/:authors_id/books/:id/edit
void edit() {
// render the right view
}
// PUT/PATCH /authors/:authors_id/books/:id
void update() {
// render the right view
}
// DELETE /authors/:authors_id/books/:id
void destroy() {
// render the right view
}
}
}
There's a lot going on up there, but it's pretty simple. Basically, for every resource, we have a controller. So for the books
resource we have the BooksController
and for the authors
resource, we have the AuthorsController
. When a request comes in to ActionPack
and a match is found, we instantiate a new instance of the controller and call the appropriate method on it. So, if we request /authors
, internally ActionPack
is doing something like this:
controller = new AuthorsController(request, response, params);
controller.index();
Pretty simple.
In depth
I'll go over the concepts more in depth here.
Controllers
Controllers are objects that represent the group of logic around a single resource. They're the "C" in MVC. In action-pack
they have play a central role of responding to requests. Every route is defined with a controller and an action. An action is a method on a controller with the signature:
void()
which is pretty easy to implement. They don't return anything (because there isn't anything listening for a return value). They just manipulate the response
object.
Actions
Actions can really perform any amount of work required to process a request. They have access to the request
object and the response
object as well as the params
.
Params
Every action has access to a @property
called params
. This has the parsed params from the route with strings as keys. So with the route:
/authors/:author_id/books
And the URL
/authors/1/books
The params
hash will look like this:
{
"params": "1"
}
Notice that the 1
is a string
and not an integer
.
respondTo
To make routing more restful, I've added an implementation of Rails #respondTo
method for ActionController
. There are two version of the #respondTo
method.
The first one is:
void respondTo(string format, void delegate() yield)
This one takes a string
as the first argument and a delegate
method. The string has to be a literal match to the current format
specified by the controller (based on the extension of the request, for example, .html
). The delegate is only called if there is a match, otherwise nothing is done.
The second is:
void respondTo(void delegate(Format) yield)
This one only takes the delegate
, but this delegate
must accept a single argument, which is a Format
object. The Format
object is very simple and provides some shortcut methods. This way you can do things like
format.html(delegate void(Format format) { // do stuff here if this an html request });
This way, in a single respondTo
delegate
method you can address multiple formats.
Routing
The goal of this is to make routing as dead simple as possible, and to avoid writing a bunch of redundant code. The vibe.d
example on routing looks like this:
shared static this()
{
auto router = new URLRouter;
router.get("/", &index);
}
void index(HTTPServerRequest req, HTTPServerResponse res)
{
res.render!("index.dt", req);
}
For a restful API this would require writing code by hand that looks very similar over and over again:
shared static this()
{
auto router = new URLRouter;
router.get("/books");
router.get("/books/:id");
router.get("/books/new");
// etc etc
}
And that only gets us through a single resource. Because I come from a Rails background I figured there has to be a way to clean this up.
Reources
Resources are at the heart of routing in action-pack
. A resource is defined with just a name, and using that as a default URL root and default controller. Every resource automatically defines several routes. The simples way to define a resource:
auto router = new ActionRouter;
router.resources("books");
Because the convention for a resource is well defined, this one line can extrapolate all the routes we intend for this resource and hook them up to the appropriate controller and actions.
Nesting
Because resources frequently belong to one another, they can be nested:
auto router = new ActionRouter;
router.resources("authors", delegate void (ActionNamespace authors) {
authors.resources("books");
});
This will generate the books
routes with a prefix automatically of authors/:author_id
.
Custom Routes
Internally when you call #resources
on ActionRouter
, it just calls a group of helper methods. All of these methods are publicly accessible, so you can use them to create custom routes.
If you want to have a GET
request for /about
(and not an entire resource), you can route it manually:
router.get("/about", "MainController", "about");
The first param is the path, the second is the name (as a string) of the controller and the last is the action to call.
Assets
Assets aren't supported right now, but there's a sort of solution:
router.assets("*");
This sets up a globally accepted route, so it should always be defined last. It takes any route and looks in the ./public
folder for a match. Like I said, not that great, but it works.
Contributing
- Fork it
- Create your feature branch (git checkout -b my-new-feature)
- Commit your changes (git commit -am 'Added some feature')
- Push to the branch (git push origin my-new-feature)
- Create new Pull Request
License
MIT.
- Registered by Jared McFarland
- ~master released 10 years ago
- jaredonline/action-pack
- MIT
- Copyright © 2013, Jared McFarland
- Authors:
- Dependencies:
- vibe-d, dunit
- Versions:
-
~master 2014-Apr-08 - Download Stats:
-
-
0 downloads today
-
0 downloads this week
-
0 downloads this month
-
197 downloads total
-
- Score:
- 1.3
- Short URL:
- action-pack.dub.pm