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 }