examples: annotate examples
[nit.git] / lib / popcorn / pop_routes.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2016 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 # Internal routes representation.
18 module pop_routes
19
20 import nitcorn
21
22 # AppRoute provide services for path and uri manipulation and matching..
23 #
24 # Default strict routes like `/` or `/user` match the same URI string.
25 # An exception is done for the trailing `/`, which is always omitted during the
26 # parsing.
27 #
28 # ~~~
29 # var route = new AppRoute("/")
30 # assert route.match("")
31 # assert route.match("/")
32 # assert not route.match("/user")
33 # assert not route.match("user")
34 #
35 # route = new AppRoute("/user")
36 # assert not route.match("/")
37 # assert route.match("/user")
38 # assert route.match("/user/")
39 # assert not route.match("/user/10")
40 # assert not route.match("/foo")
41 # assert not route.match("user")
42 # assert not route.match("/username")
43 # ~~~
44 class AppRoute
45
46 # Route relative path from server root.
47 var path: String
48
49 # Does self match the `req`?
50 fun match(uri: String): Bool do
51 uri = uri.simplify_path
52 var path = resolve_path(uri)
53 if uri.is_empty and path == "/" then return true
54 return uri == path
55 end
56
57 # Replace path parameters with concrete values from the `uri`.
58 #
59 # For strict routes, it returns the path unchanged:
60 # ~~~
61 # var route = new AppRoute("/")
62 # assert route.resolve_path("/user/10/profile") == "/"
63 #
64 # route = new AppRoute("/user")
65 # assert route.resolve_path("/user/10/profile") == "/user"
66 # ~~~
67 fun resolve_path(uri: String): String do return path.simplify_path
68
69 # Remove `resolved_path` prefix from `uri`.
70 #
71 # Mainly used to resolve and match mountable routes.
72 #
73 # ~~~
74 # var route = new AppRoute("/")
75 # assert route.uri_root("/user/10/profile") == "/user/10/profile"
76 #
77 # route = new AppRoute("/user")
78 # assert route.uri_root("/user/10/profile") == "/10/profile"
79 # ~~~
80 fun uri_root(uri: String): String do
81 var path = resolve_path(uri)
82 if path == "/" then return uri
83 return uri.substring(path.length, uri.length).simplify_path
84 end
85 end
86
87 # Parameterizable routes.
88 #
89 # Routes that can contains variables parts that will be resolved during the
90 # matching process.
91 #
92 # Route parameters are marked with a colon `:`
93 # ~~~
94 # var route = new AppParamRoute("/:id")
95 # assert not route.match("/")
96 # assert route.match("/user")
97 # assert route.match("/user/")
98 # assert not route.match("/user/10")
99 # ~~~
100 #
101 # It is possible to use more than one parameter in the same route:
102 # ~~~
103 # route = new AppParamRoute("/user/:userId/items/:itemId")
104 # assert not route.match("/user/10/items/")
105 # assert route.match("/user/10/items/e895346")
106 # assert route.match("/user/USER/items/0/")
107 # assert not route.match("/user/10/items/10/profile")
108 # ~~~
109 class AppParamRoute
110 super AppRoute
111
112 init do parse_path_parameters(path)
113
114 # Cut `path` into `UriParts`.
115 fun parse_path_parameters(path: String) do
116 for part in path.split("/") do
117 if not part.is_empty and part.first == ':' then
118 # is an uri param
119 path_parts.add new UriParam(part.substring(1, part.length))
120 else
121 # is a standard string
122 path_parts.add new UriString(part)
123 end
124 end
125 end
126
127 # For parameterized routes, parameter names are replaced by their value in the URI.
128 # ~~~
129 # var route = new AppParamRoute("/user/:id")
130 # assert route.resolve_path("/user/10/profile") == "/user/10"
131 #
132 # route = new AppParamRoute("/user/:userId/items/:itemId")
133 # assert route.resolve_path("/user/Morriar/items/i156/desc") == "/user/Morriar/items/i156"
134 # ~~~
135 redef fun resolve_path(uri) do
136 var uri_params = parse_uri_parameters(uri)
137 var path = "/"
138 for part in path_parts do
139 if part isa UriString then
140 path /= part.string
141 else if part isa UriParam then
142 path /= uri_params.get_or_default(part.name, part.name)
143 end
144 end
145 return path.simplify_path
146 end
147
148 # Extract parameter values from `uri`.
149 # ~~~
150 # var route = new AppParamRoute("/user/:userId/items/:itemId")
151 # var params = route.parse_uri_parameters("/user/10/items/i125/desc")
152 # assert params["userId"] == "10"
153 # assert params["itemId"] == "i125"
154 # assert params.length == 2
155 #
156 # params = route.parse_uri_parameters("/")
157 # assert params.is_empty
158 # ~~~
159 fun parse_uri_parameters(uri: String): Map[String, String] do
160 var res = new HashMap[String, String]
161 if path_parts.is_empty then return res
162 var parts = uri.split("/")
163 for i in [0 .. path_parts.length[ do
164 if i >= parts.length then return res
165 var ppart = path_parts[i]
166 var part = parts[i]
167 if not ppart.match(part) then return res
168 if ppart isa UriParam then
169 res[ppart.name] = part
170 end
171 end
172 return res
173 end
174
175 private var path_parts = new Array[UriPart]
176 end
177
178 # Route with glob.
179 #
180 # Route variable part is suffixed with a star `*`:
181 # ~~~
182 # var route = new AppGlobRoute("/*")
183 # assert route.match("/")
184 # assert route.match("/user")
185 # assert route.match("/user/10")
186 # ~~~
187 #
188 # Glob routes can be combined with parameters:
189 # ~~~
190 # route = new AppGlobRoute("/user/:id/*")
191 # assert not route.match("/user")
192 # assert route.match("/user/10")
193 # assert route.match("/user/10/profile")
194 # ~~~
195 #
196 # Note that the star can be used directly on the end of an URI part:
197 # ~~~
198 # route = new AppGlobRoute("/user*")
199 # assert route.match("/user")
200 # assert route.match("/username")
201 # assert route.match("/user/10/profile")
202 # assert not route.match("/foo")
203 # ~~~
204 #
205 # For now, stars cannot be used inside a route, use URI parameters instead.
206 class AppGlobRoute
207 super AppParamRoute
208
209 # Path without the trailing `*`.
210 # ~~~
211 # var route = new AppGlobRoute("/user/:id/*")
212 # assert route.resolve_path("/user/10/profile") == "/user/10"
213 #
214 # route = new AppGlobRoute("/user/:userId/items/:itemId*")
215 # assert route.resolve_path("/user/Morriar/items/i156/desc") == "/user/Morriar/items/i156"
216 # ~~~
217 redef fun resolve_path(uri) do
218 var path = super
219 if path.has_suffix("*") then
220 return path.substring(0, path.length - 1).simplify_path
221 end
222 return path.simplify_path
223 end
224
225 redef fun match(uri) do
226 var path = resolve_path(uri)
227 return uri.has_prefix(path.substring(0, path.length - 1))
228 end
229 end
230
231 # A String that compose an URI.
232 #
233 # In practice, UriPart can be parameters or static strings.
234 private interface UriPart
235 # Does `self` matches a part of the uri?
236 fun match(uri_part: String): Bool is abstract
237 end
238
239 # An uri parameter string like `:id`.
240 private class UriParam
241 super UriPart
242
243 # Param `name` in the route uri.
244 var name: String
245
246 # Parameters match everything.
247 redef fun match(part) do return not part.is_empty
248
249 redef fun to_s do return name
250 end
251
252 # A static uri string like `users`.
253 private class UriString
254 super UriPart
255
256 # Uri part string.
257 var string: String
258
259 # Empty strings match everything otherwise matching is based on string equality.
260 redef fun match(part) do return string.is_empty or string == part
261
262 redef fun to_s do return string
263 end