1 module action_dispatch.router;
2 
3 public import std.variant;
4 public import std.conv;
5 public import std.traits;
6 public import std.typecons;
7 
8 import vibe.d;
9 import std.stdio;
10 
11 import action_dispatch.all;
12 
13 class ActionRouter : HTTPServerRequestHandler {
14 
15   private {
16     ActionRoute[][HTTPMethod.max + 1] _routes;
17   }
18 
19   void handleRequest(HTTPServerRequest req, HTTPServerResponse res) {
20     auto method = req.method;
21     auto route  = route(method, req.path, req.params);
22 
23     if (!(route is null)) {
24       route.controller.handleRequest(req, res, route.action);
25     }
26   }
27 
28   ActionRoute route(HTTPMethod method, string path, ref string[string] params) {
29     string format = fetchFormat(path, params);
30 
31     while (true) {
32       if (auto pr = &_routes[method]) {
33         foreach( ref r; *pr ) {
34           if (r.matches(path, params)) {
35             return r;
36           }
37         }
38       }
39       if (method == HTTPMethod.HEAD) method = HTTPMethod.GET;
40       else break;
41     }
42     return null;
43   }
44 
45   ActionRouter resources(string resources, string prefix = "") {
46     string controller = controllerizeString(resources);
47 
48     if (prefix.length > 0)
49       prefix = "/" ~ prefix;
50 
51     // this is our routing table for a resource
52     get(   prefix ~ "/" ~ resources,               controller, "index");
53     get(   prefix ~ "/" ~ resources ~ "/new",      controller, "init");
54     post(  prefix ~ "/" ~ resources,               controller, "create");
55     get(   prefix ~ "/" ~ resources ~ "/:id",      controller, "show");
56     get(   prefix ~ "/" ~ resources ~ "/:id/edit", controller, "edit");
57     patch( prefix ~ "/" ~ resources ~ "/:id",      controller, "update");
58     put(   prefix ~ "/" ~ resources ~ "/:id",      controller, "update");
59     del(   prefix ~ "/" ~ resources ~ "/:id",      controller, "destroy");
60     return this;
61   }
62 
63   ActionRouter resources(string resource, void delegate(ActionNamespace namespace) yield) {
64     resources(resource);
65 
66     auto namespace = new ActionNamespace(this, resource);
67     yield(namespace);
68     return this;
69   }
70 
71   ActionRouter get(string route, string controller, string action) {
72     match(HTTPMethod.GET, route, controller, action);
73     return this;
74   }
75 
76   ActionRouter put(string route, string controller, string action) {
77     match(HTTPMethod.PUT, route, controller, action);
78     return this;
79   }
80 
81   ActionRouter patch(string route, string controller, string action) {
82     match(HTTPMethod.PATCH, route, controller, action);
83     return this;
84   }
85 
86   ActionRouter post(string route, string controller, string action) {
87     match(HTTPMethod.POST, route, controller, action);
88     return this;
89   }
90 
91   ActionRouter del(string route, string controller, string action) {
92     match(HTTPMethod.DELETE, route, controller, action);
93     return this;
94   }
95 
96   ActionRouter assets(string route) {
97     match(HTTPMethod.GET, route, "ActionController", "assets");
98     return this;
99   }
100 
101   ActionRouter match(HTTPMethod method, string path, string controller, string action) {
102     assert(count(path, ':') <= ActionRoute.maxRouteParameters, "Too many route parameters");
103     auto route = new ActionRoute(method, path, controller, action);
104     _routes[method] ~= route;
105     return this;
106   }
107 
108   @property typeof(_routes) routes() {
109     return _routes;
110   }
111 
112   string controllerizeString(string resource) {
113     string retValue = "";
114 
115     foreach(str; split(resource, "_")) {
116       retValue ~= str.capitalize;
117     }
118     return retValue ~ "Controller";
119   }
120 
121   private {
122     string fetchFormat(ref string path, ref string[string] params) {
123       string format;
124       ptrdiff_t index = lastIndexOf(path, '.');
125       if (index > -1) {
126         format = path[index + 1..path.length];
127         path   = path[0..index];
128       } else {
129         format = "html";
130       }
131 
132       params["format"] = format;
133       return format;
134     }
135   }
136 }
137 
138 unittest {
139   import dunit.toolkit;
140 
141   string[string] params;
142   auto router = new ActionRouter;
143   router.get("/", "MainController", "index");
144 
145   router.controllerizeString("foos").assertEqual("FoosController");
146   router.controllerizeString("classes").assertEqual("ClassesController");
147   router.controllerizeString("toes").assertEqual("ToesController");
148   router.controllerizeString("foo_bars").assertEqual("FooBarsController");
149 
150   router.resources("foos");
151   router.route(HTTPMethod.GET, "/foos", params).assertInstanceOf!(ActionRoute)();
152   router.route(HTTPMethod.GET, "/foos/1", params).assertInstanceOf!(ActionRoute)();
153   router.resources("spreads");
154 
155   router.resources("authors", delegate void (ActionNamespace authors) {
156     authors.resources("books");
157   });
158 
159   router.route(HTTPMethod.GET, "/authors/1/books/12", params).assertInstanceOf!(ActionRoute)();
160   params["author_id"].assertEqual("1");
161   params["id"].assertEqual("12");
162 
163   router.resources("bloggers", delegate void (ActionNamespace bloggers) {
164     bloggers.resources("blogs", delegate void (ActionNamespace blogs) {
165       blogs.resources("comments", delegate void(ActionNamespace comments) {
166         comments.resources("responses");
167       });
168     });
169   });
170 
171   router.route(HTTPMethod.GET, "/bloggers/1/blogs/2/comments/3", params).assertInstanceOf!(ActionRoute)();
172   router.route(HTTPMethod.GET, "/bloggers/1/blogs/2/comments/3/responses", params).assertInstanceOf!(ActionRoute)();
173 
174   params["blogger_id"].assertEqual("1");
175   params["blog_id"].assertEqual("2");
176   params["comment_id"].assertEqual("3");
177 
178   router.route(HTTPMethod.GET, "/bloggers.json", params).assertInstanceOf!(ActionRoute)();
179   params["format"].assertEqual("json");
180 
181   router.assets("*");
182   auto r = router.route(HTTPMethod.GET, "/js/all.js", params);
183   r.assertInstanceOf!(ActionRoute)();
184   r.controllerName.assertEqual("ActionController");
185   r.action.assertEqual("assets");
186 }