1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
17 # Internal routes representation.
22 # AppRoute provide services for path and uri manipulation and matching..
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
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")
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")
46 # Route relative path from server root.
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
57 # Replace path parameters with concrete values from the `uri`.
59 # For strict routes, it returns the path unchanged:
61 # var route = new AppRoute("/")
62 # assert route.resolve_path("/user/10/profile") == "/"
64 # route = new AppRoute("/user")
65 # assert route.resolve_path("/user/10/profile") == "/user"
67 fun resolve_path
(uri
: String): String do return path
.simplify_path
69 # Remove `resolved_path` prefix from `uri`.
71 # Mainly used to resolve and match mountable routes.
74 # var route = new AppRoute("/")
75 # assert route.uri_root("/user/10/profile") == "/user/10/profile"
77 # route = new AppRoute("/user")
78 # assert route.uri_root("/user/10/profile") == "/10/profile"
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
87 # Parameterizable routes.
89 # Routes that can contains variables parts that will be resolved during the
92 # Route parameters are marked with a colon `:`
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")
101 # It is possible to use more than one parameter in the same route:
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")
112 init do parse_path_parameters
(path
)
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
119 path_parts
.add
new UriParam(part
.substring
(1, part
.length
))
121 # is a standard string
122 path_parts
.add
new UriString(part
)
127 # For parameterized routes, parameter names are replaced by their value in the URI.
129 # var route = new AppParamRoute("/user/:id")
130 # assert route.resolve_path("/user/10/profile") == "/user/10"
132 # route = new AppParamRoute("/user/:userId/items/:itemId")
133 # assert route.resolve_path("/user/Morriar/items/i156/desc") == "/user/Morriar/items/i156"
135 redef fun resolve_path
(uri
) do
136 var uri_params
= parse_uri_parameters
(uri
)
138 for part
in path_parts
do
139 if part
isa UriString then
141 else if part
isa UriParam then
142 path
/= uri_params
.get_or_default
(part
.name
, part
.name
)
145 return path
.simplify_path
148 # Extract parameter values from `uri`.
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
156 # params = route.parse_uri_parameters("/")
157 # assert params.is_empty
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
]
167 if not ppart
.match
(part
) then return res
168 if ppart
isa UriParam then
169 res
[ppart
.name
] = part
175 private var path_parts
= new Array[UriPart]
180 # Route variable part is suffixed with a star `*`:
182 # var route = new AppGlobRoute("/*")
183 # assert route.match("/")
184 # assert route.match("/user")
185 # assert route.match("/user/10")
188 # Glob routes can be combined with parameters:
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")
196 # Note that the star can be used directly on the end of an URI part:
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")
205 # For now, stars cannot be used inside a route, use URI parameters instead.
209 # Path without the trailing `*`.
211 # var route = new AppGlobRoute("/user/:id/*")
212 # assert route.resolve_path("/user/10/profile") == "/user/10"
214 # route = new AppGlobRoute("/user/:userId/items/:itemId*")
215 # assert route.resolve_path("/user/Morriar/items/i156/desc") == "/user/Morriar/items/i156"
217 redef fun resolve_path
(uri
) do
219 if path
.has_suffix
("*") then
220 return path
.substring
(0, path
.length
- 1).simplify_path
222 return path
.simplify_path
225 redef fun match
(uri
) do
226 var path
= resolve_path
(uri
)
227 return uri
.has_prefix
(path
.substring
(0, path
.length
- 1))
231 # A String that compose an URI.
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
239 # An uri parameter string like `:id`.
240 private class UriParam
243 # Param `name` in the route uri.
246 # Parameters match everything.
247 redef fun match
(part
) do return not part
.is_empty
249 redef fun to_s
do return name
252 # A static uri string like `users`.
253 private class UriString
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
262 redef fun to_s
do return string