examples: annotate examples
[nit.git] / lib / nitcorn / vararg_routes.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2014 Alexandre Terrasa <alexandre@moz-code.org>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Routes with parameters.
18 #
19 # Using `vararg_routes`, a `Route` path can contain variable parts
20 # that will be matched against a `HttpRequest` URL.
21 #
22 # Variable parameters of a route path can be specified using the `:` prefix:
23 #
24 # ~~~nitish
25 # var iface = "http://localhost:3000"
26 # var vh = new VirtualHost(iface)
27 # vh.routes.add new Route("/blog/articles/:articleId", new BlogArticleAction)
28 # ~~~
29 #
30 # Route arguments can be accessed from the `HttpRequest` within a nitcorn `Action`:
31 #
32 # ~~~nitish
33 # class BlogArticleAction
34 # super Action
35 #
36 # redef fun answer(request, url) do
37 # var param = request.param("articleId")
38 # if param == null then
39 # return new HttpResponse(400)
40 # end
41 #
42 # print url # let's say "/blog/articles/12"
43 # print param # 12
44 #
45 # return new HttpResponse(200)
46 # end
47 # end
48 # ~~~
49 #
50 # ## Route matching
51 #
52 # Route can match variables expression.
53 #
54 # ~~~
55 # # We need an Action to try routes.
56 # class DummyAction super Action end
57 # var action = new DummyAction
58 #
59 # var route = new Route("/users/:id", action)
60 # assert not route.match("/users")
61 # assert route.match("/users/1234")
62 # assert route.match("/users/") # empty id
63 # ~~~
64 #
65 # Route without uri parameters still behave like before.
66 #
67 # ~~~
68 # route = new Route("/users", action)
69 # assert route.match("/users")
70 # assert route.match("/users/1234")
71 # assert not route.match("/issues/1234")
72 # ~~~
73 #
74 # ## Route priority
75 #
76 # Priority depends on the order the routes were added to the `Routes` dispatcher.
77 #
78 # ~~~
79 # var host = new VirtualHost("")
80 # var routes = new Routes(host)
81 #
82 # routes.add new Route("/:a/:b/:c", action)
83 # routes.add new Route("/users/:id", action)
84 # routes.add new Route("/:foo", action)
85 #
86 # assert routes["/a/b/c"].path == "/:a/:b/:c"
87 # assert routes["/a/b/c/d"].path == "/:a/:b/:c"
88 # assert routes["/users/1234/foo"].path == "/:a/:b/:c"
89 #
90 # assert routes["/users/"].path == "/users/:id"
91 # assert routes["users/"].path == "/users/:id"
92 # assert routes["/users/1234"].path == "/users/:id"
93 #
94 # assert routes["/users"].path == "/:foo"
95 # assert routes["/"].path == "/:foo"
96 # assert routes[""].path == "/:foo"
97 # ~~~
98 #
99 # ## Accessing uri parameter and values
100 #
101 # Parameters can be accessed by parsing the uri.
102 #
103 # ~~~
104 # route = new Route("/users/:id", action)
105 # var params = route.parse_params("/users/1234")
106 # assert params.has_key("id")
107 # assert not params.has_key("foo")
108 # assert params["id"] == "1234"
109 # ~~~
110 #
111 # Or from the `HttpRequest`.
112 #
113 # ~~~
114 # route = new Route("/users/:id", action)
115 # var req = new HttpRequest
116 # req.uri_params = route.parse_params("/users/1234")
117 # assert req.params == ["id"]
118 # assert req.param("id") == "1234"
119 # assert req.param("foo") == null
120 # ~~~
121 module vararg_routes
122
123 import server_config
124 import http_request
125
126 # A route to an `Action` according to a `path`
127 redef class Route
128
129 redef init do
130 super
131 parse_pattern(path)
132 end
133
134 # Replace `self.path` parameters with concrete values from the `request` URI.
135 fun resolve_path(request: HttpRequest): nullable String do
136 if pattern_parts.is_empty then return self.path
137 var path = "/"
138 for part in pattern_parts do
139 if part isa UriString then
140 path /= part.string
141 else if part isa UriParam then
142 path /= request.param(part.name) or else part.name
143 end
144 end
145 return path
146 end
147
148 # Cut `path` into `UriParts`.
149 private fun parse_pattern(path: nullable String) do
150 if path == null then return
151 path = standardize_path(path)
152 var parts = path.split("/")
153 for part in parts do
154 if not part.is_empty and part.first == ':' then
155 # is an uri param
156 var name = part.substring(1, part.length)
157 var param = new UriParam(name)
158 pattern_parts.add param
159 else
160 # is a standard string
161 pattern_parts.add new UriString(part)
162 end
163 end
164 end
165
166 # `UriPart` forming `self` pattern.
167 private var pattern_parts = new Array[UriPart]
168
169 # Does `self` matches `uri`?
170 fun match(uri: nullable String): Bool do
171 if pattern_parts.is_empty then return true
172 if uri == null then return false
173 uri = standardize_path(uri)
174 var parts = uri.split("/")
175 for i in [0 .. pattern_parts.length[ do
176 if i >= parts.length then return false
177 var ppart = pattern_parts[i]
178 var part = parts[i]
179 if not ppart.match(part) then return false
180 end
181 return true
182 end
183
184 # Extract parameter values from `uri`.
185 fun parse_params(uri: nullable String): Map[String, String] do
186 var res = new HashMap[String, String]
187 if pattern_parts.is_empty then return res
188 if uri == null then return res
189 uri = standardize_path(uri)
190 var parts = uri.split("/")
191 for i in [0 .. pattern_parts.length[ do
192 if i >= parts.length then return res
193 var ppart = pattern_parts[i]
194 var part = parts[i]
195 if not ppart.match(part) then return res
196 if ppart isa UriParam then
197 res[ppart.name] = part
198 end
199 end
200 return res
201 end
202
203 # Remove first occurence of `/`.
204 private fun standardize_path(path: String): String do
205 if not path.is_empty and path.first == '/' then
206 return path.substring(1, path.length)
207 end
208 return path
209 end
210 end
211
212 # A String that compose an URI.
213 #
214 # In practice, UriPart can be parameters or static strings.
215 private interface UriPart
216 # Does `self` matches a part of the uri?
217 fun match(uri_part: String): Bool is abstract
218 end
219
220 # An uri parameter string like `:id`.
221 private class UriParam
222 super UriPart
223
224 # Param `name` in the route uri.
225 var name: String
226
227 # Parameters match everything.
228 redef fun match(part) do return true
229
230 redef fun to_s do return name
231 end
232
233 # A static uri string like `users`.
234 private class UriString
235 super UriPart
236
237 # Uri part string.
238 var string: String
239
240 # Empty strings match everything otherwise matching is based on string equality.
241 redef fun match(part) do return string.is_empty or string == part
242
243 redef fun to_s do return string
244 end
245
246 redef class Routes
247 # Use `Route::match` instead of `==`.
248 redef fun [](key) do
249 for route in routes do
250 if route.match(key) then return route
251 end
252 return null
253 end
254 end
255
256 redef class HttpRequest
257
258 # Parameters found in uri associated to their values.
259 var uri_params: Map[String, String] = new HashMap[String, String] is public writable
260
261 # Get the value for parameter `name` or `null`.
262 fun param(name: String): nullable String do
263 if not uri_params.has_key(name) then return null
264 return uri_params[name]
265 end
266
267 # List all uri parameters matched by this request.
268 fun params: Array[String] do return uri_params.keys.to_a
269 end