1 module action_dispatch.route;
2 
3 import action_dispatch.all;
4 
5 class ActionRoute {
6   protected {
7     string           _path;
8     string           _controllerName;
9     string           _action;
10     HTTPMethod       _method;
11     ActionController _controller;
12   }
13 
14   public static {
15     enum maxRouteParameters = 64;
16   }
17 
18   this(HTTPMethod method, string path, string controllerName, string action) {
19     _controllerName = controllerName;
20     _method         = method;
21     _path           = path;
22     _action         = action;
23   }
24 
25   @property string controllerName() {
26     return _controllerName;
27   }
28 
29   @property string path() {
30     return _path;
31   }
32 
33   @property HTTPMethod method() {
34     return _method;
35   }
36 
37   @property string action() {
38     return _action;
39   }
40 
41   @property ActionController controller() {
42     if (_controller is null) {
43       _controller = ActionController.loadController(_controllerName);
44     }
45 
46     return _controller;
47   }
48 
49   override string toString() {
50     string method;
51     if (_method == HTTPMethod.GET)
52       method = "GET";
53     else if (_method == HTTPMethod.PUT)
54       method = "PUT";
55     else if (_method == HTTPMethod.POST)
56       method = "POST";
57     else if (_method == HTTPMethod.PATCH)
58       method = "PATCH";
59     else if (_method == HTTPMethod.DELETE)
60       method = "DELETE";
61 
62     return "#" ~ method ~ "\t\t" ~ _path ~ "\t\t" ~ _controllerName ~ "\t\t#" ~ _action;
63   }
64 
65   bool matches(string url, ref string[string] params) const {
66     size_t i, j;
67 
68     Tuple!(string, string)[maxRouteParameters] tmpparams;
69     size_t tmpparams_length = 0;
70 
71     // if the url matches the path totally, just return true;
72     // that means there are no variables in the url
73     if (url == _path)
74       return true;
75 
76     // if there's not a direct match, loop through looking
77     // for a wildcard or variable declaration
78     for (i = 0, j = 0; i < url.length && j < _path.length;) {
79 
80       // if we hit a wildcard add any accumulated tmpparams
81       // to the params dictionary
82       if (_path[j] == '*') {
83         foreach (t; tmpparams[0..tmpparams_length])
84           params[t[0]] = t[1];
85         return true;
86       }
87 
88       // if the current index in the url and the _path match
89       // continue to the next index for each
90       if (url[i] == _path[j]) {
91         i++;
92         j++;
93       }
94       // if we encounter a variable we need to find out what it
95       // is called, and what the value is. We store it in the tmp
96       // params dictionary until we verify the full match
97       else if (_path[j] == ':') {
98         j++;
99         string name =  skipPathNode(_path, j);
100         string match = skipPathNode(url, i);
101         assert(tmpparams_length < maxRouteParameters, "Maximum number of route parameters exceeded.");
102         tmpparams[tmpparams_length++] = tuple(name, urlDecode(match));
103       }
104       // if we get this far we don't have a match so we exit
105       else return false;
106     }
107 
108     // if we exit the for loop and get here we just need to move the
109     // tmp params to the actual params dictionary and return true
110     if ((j < _path.length && _path[j] == '*') || (i == url.length && j == _path.length)) {
111       foreach (t; tmpparams[0..tmpparams_length])
112           params[t[0]] = t[1];
113       return true;
114     }
115 
116     // falling all the way through gives us a false hit on the match
117     return false;
118   }
119 }
120 
121 // helper method to just jump forward a variable name
122 // or a value name
123 private string skipPathNode(string str, ref size_t idx) {
124   size_t start = idx;
125   while ( idx < str.length && str[idx] != '/' ) idx++;
126   return str[start .. idx];
127 }
128 
129 unittest {
130   import dunit.toolkit;
131 
132   string[string] params;
133   auto route = new ActionRoute(HTTPMethod.GET, "/", "MainController", "index");
134 
135   route.path.assertEqual("/");
136   route.controllerName.assertEqual("MainController");
137   route.method.assertEqual(HTTPMethod.GET);
138   route.action.assertEqual("index");
139 
140   route.matches("/", params).assertEqual(true);
141   route.matches("/foo", params).assertEqual(false);
142 
143   auto dynamic_route = new ActionRoute(HTTPMethod.GET, "/users/:id", "UsersController", "show");
144   dynamic_route.matches("/users/1", params).assertEqual(true);
145   params["id"].assertEqual("1");
146 
147   auto star_route = new ActionRoute(HTTPMethod.GET, "/dog/*", "DogsController", "index");
148   star_route.matches("/dog/foo/bar", params).assertEqual(true);
149   star_route.matches("/cat/foo/bar", params).assertEqual(false);
150 }