lib/popcorn: introduce Router::use_before service
[nit.git] / lib / popcorn / pop_handlers.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 # Route handlers.
18 module pop_handlers
19
20 import pop_routes
21 import json
22
23 # Class handler for a route.
24 #
25 # **Routing** refers to determining how an application responds to a client request
26 # to a particular endpoint, which is a URI (or path) and a specific HTTP request
27 # method GET, POST, PUT or DELETE (other methods are not suported yet).
28 #
29 # Each route can have one or more handler methods, which are executed when the route is matched.
30 #
31 # Route handlers definition takes the following form:
32 #
33 # ~~~nitish
34 # class MyHandler
35 # super Handler
36 #
37 # redef fun METHOD(req, res) do end
38 # end
39 # ~~~
40 #
41 # Where:
42 # * `MyHandler` is the name of the handler you will add to the app.
43 # * `METHOD` can be replaced by `get`, `post`, `put` or `delete`.
44 #
45 # The following example responds with `Hello World!` to GET and POST requests:
46 #
47 # ~~~
48 # class MyHandler
49 # super Handler
50 #
51 # redef fun get(req, res) do res.send "Got a GET request"
52 # redef fun post(req, res) do res.send "Got a POST request"
53 # end
54 # ~~~
55 #
56 # To make your handler responds to a specific route, you have to add it to the app.
57 #
58 # Respond to POST request on the root route (`/`), the application's home page:
59 #
60 # ~~~
61 # var app = new App
62 # app.use("/", new MyHandler)
63 # ~~~
64 #
65 # Respond to a request to the `/user` route:
66 #
67 # ~~~
68 # app.use("/user", new MyHandler)
69 # ~~~
70 abstract class Handler
71
72 # Call `all(req, res)` if `route` matches `uri`.
73 private fun handle(route: AppRoute, uri: String, req: HttpRequest, res: HttpResponse) do
74 if route.match(uri) then
75 if route isa AppParamRoute then
76 req.uri_params = route.parse_uri_parameters(uri)
77 end
78 all(req, res)
79 end
80 end
81
82 # Handler to all kind of HTTP request methods.
83 #
84 # `all` is a special request handler, which is not derived from any
85 # HTTP method. This method is used to respond at a path for all request methods.
86 #
87 # In the following example, the handler will be executed for requests to "/user"
88 # whether you are using GET, POST, PUT, DELETE, or any other HTTP request method.
89 #
90 # ~~~
91 # class AllHandler
92 # super Handler
93 #
94 # redef fun all(req, res) do res.send "Every request to the homepage"
95 # end
96 # ~~~
97 #
98 # Using the `all` method you can also implement other HTTP request methods.
99 #
100 # ~~~
101 # class MergeHandler
102 # super Handler
103 #
104 # redef fun all(req, res) do
105 # if req.method == "MERGE" then
106 # # handle that method
107 # else super # keep handle GET, POST, PUT and DELETE methods
108 # end
109 # end
110 # ~~~
111 fun all(req: HttpRequest, res: HttpResponse) do
112 if req.method == "GET" then
113 get(req, res)
114 else if req.method == "POST" then
115 post(req, res)
116 else if req.method == "PUT" then
117 put(req, res)
118 else if req.method == "DELETE" then
119 delete(req, res)
120 else
121 res.status_code = 405
122 end
123 end
124
125 # GET handler.
126 #
127 # Exemple of route responding to GET requests.
128 # ~~~
129 # class GetHandler
130 # super Handler
131 #
132 # redef fun get(req, res) do res.send "GETrequest received"
133 # end
134 # ~~~
135 fun get(req: HttpRequest, res: HttpResponse) do end
136
137 # POST handler.
138 #
139 # Exemple of route responding to POST requests.
140 # ~~~
141 # class PostHandler
142 # super Handler
143 #
144 # redef fun post(req, res) do res.send "POST request received"
145 # end
146 # ~~~
147 fun post(req: HttpRequest, res: HttpResponse) do end
148
149 # PUT handler.
150 #
151 # Exemple of route responding to PUT requests.
152 # ~~~
153 # class PutHandler
154 # super Handler
155 #
156 # redef fun put(req, res) do res.send "PUT request received"
157 # end
158 # ~~~
159 fun put(req: HttpRequest, res: HttpResponse) do end
160
161 # DELETE handler.
162 #
163 # Exemple of route responding to PUT requests.
164 # ~~~
165 # class DeleteHandler
166 # super Handler
167 #
168 # redef fun delete(req, res) do res.send "DELETE request received"
169 # end
170 # ~~~
171 fun delete(req: HttpRequest, res: HttpResponse) do end
172 end
173
174 # Static files server.
175 #
176 # To serve static files such as images, CSS files, and JavaScript files, use the
177 # Popcorn built-in handler `StaticHandler`.
178 #
179 # Pass the name of the directory that contains the static assets to the StaticHandler
180 # init method to start serving the files directly.
181 # For example, use the following code to serve images, CSS files, and JavaScript files
182 # in a directory named `public`:
183 #
184 # ~~~
185 # var app = new App
186 # app.use("/", new StaticHandler("public/"))
187 # ~~~
188 #
189 # Now, you can load the files that are in the `public` directory:
190 #
191 # ~~~raw
192 # http://localhost:3000/images/trollface.jpg
193 # http://localhost:3000/css/style.css
194 # http://localhost:3000/js/app.js
195 # http://localhost:3000/hello.html
196 # ~~~
197 #
198 # Popcorn looks up the files relative to the static directory, so the name of the
199 # static directory is not part of the URL.
200 # To use multiple static assets directories, add the `StaticHandler` multiple times:
201 #
202 # ~~~
203 # app.use("/", new StaticHandler("public/"))
204 # app.use("/", new StaticHandler("files/"))
205 # ~~~
206 #
207 # Popcorn looks up the files in the order in which you set the static directories
208 # with the `use` method.
209 #
210 # To create a virtual path prefix (where the path does not actually exist in the file system)
211 # for files that are served by the `StaticHandler`, specify a mount path for the
212 # static directory, as shown below:
213 #
214 # ~~~
215 # app.use("/static/", new StaticHandler("public/"))
216 # ~~~
217 #
218 # Now, you can load the files that are in the public directory from the `/static`
219 # path prefix.
220 #
221 # ~~~raw
222 # http://localhost:3000/static/images/trollface.jpg
223 # http://localhost:3000/static/css/style.css
224 # http://localhost:3000/static/js/app.js
225 # http://localhost:3000/static/hello.html
226 # ~~~
227 #
228 # However, the path that you provide to the `StaticHandler` is relative to the
229 # directory from where you launch your app.
230 # If you run the app from another directory, it’s safer to use the absolute path of
231 # the directory that you want to serve.
232 class StaticHandler
233 super Handler
234
235 # Static files directory to serve.
236 var static_dir: String
237
238 # Internal file server used to lookup and render files.
239 var file_server: FileServer is lazy do
240 var srv = new FileServer(static_dir)
241 srv.show_directory_listing = false
242 return srv
243 end
244
245 redef fun handle(route, uri, req, res) do
246 var answer = file_server.answer(req, route.uri_root(uri))
247 if answer.status_code == 200 then
248 res.status_code = answer.status_code
249 res.header.add_all answer.header
250 res.files.add_all answer.files
251 res.send
252 else if answer.status_code != 404 then
253 res.status_code = answer.status_code
254 end
255 end
256 end
257
258 # Mountable routers
259 #
260 # Use the `Router` class to create modular, mountable route handlers.
261 # A Router instance is a complete middleware and routing system; for this reason,
262 # it is often referred to as a “mini-app”.
263 #
264 # The following example creates a router as a module, loads a middleware handler in it,
265 # defines some routes, and mounts the router module on a path in the main app.
266 #
267 # ~~~
268 # class AppHome
269 # super Handler
270 #
271 # redef fun get(req, res) do res.send "Site Home"
272 # end
273 #
274 # class UserLogger
275 # super Handler
276 #
277 # redef fun all(req, res) do print "User logged"
278 # end
279 #
280 # class UserHome
281 # super Handler
282 #
283 # redef fun get(req, res) do res.send "User Home"
284 # end
285 #
286 # class UserProfile
287 # super Handler
288 #
289 # redef fun get(req, res) do res.send "User Profile"
290 # end
291 #
292 # var user_router = new Router
293 # user_router.use("/*", new UserLogger)
294 # user_router.use("/", new UserHome)
295 # user_router.use("/profile", new UserProfile)
296 #
297 # var app = new App
298 # app.use("/", new AppHome)
299 # app.use("/user", user_router)
300 # ~~~
301 #
302 # The app will now be able to handle requests to /user and /user/profile, as well
303 # as call the `Time` middleware handler that is specific to the route.
304 class Router
305 super Handler
306
307 # List of handlers to match with requests.
308 private var handlers = new Map[AppRoute, Handler]
309
310 # List of handlers to match before every other.
311 private var pre_handlers = new Map[AppRoute, Handler]
312
313 # Register a `handler` for a route `path`.
314 #
315 # Route paths are matched in registration order.
316 fun use(path: String, handler: Handler) do
317 var route = build_route(handler, path)
318 handlers[route] = handler
319 end
320
321 # Register a pre-handler for a route `path`.
322 #
323 # Prehandlers are matched before every other handlers in registrastion order.
324 fun use_before(path: String, handler: Handler) do
325 var route = build_route(handler, path)
326 pre_handlers[route] = handler
327 end
328
329 redef fun handle(route, uri, req, res) do
330 if not route.match(uri) then return
331 handle_pre(route, uri, req, res)
332 handle_in(route, uri, req, res)
333
334 private fun handle_pre(route: AppRoute, uri: String, req: HttpRequest, res: HttpResponse) do
335 for hroute, handler in pre_handlers do
336 handler.handle(hroute, route.uri_root(uri), req, res)
337 end
338 end
339
340 private fun handle_in(route: AppRoute, uri: String, req: HttpRequest, res: HttpResponse) do
341 for hroute, handler in handlers do
342 handler.handle(hroute, route.uri_root(uri), req, res)
343 if res.sent then break
344 end
345 end
346
347 private fun build_route(handler: Handler, path: String): AppRoute do
348 if handler isa Router or handler isa StaticHandler then
349 return new AppGlobRoute(path)
350 else if path.has_suffix("*") then
351 return new AppGlobRoute(path)
352 else
353 return new AppParamRoute(path)
354 end
355 end
356 end
357
358 # Popcorn application.
359 #
360 # The `App` is the main point of the application.
361 # It acts as a `Router` that holds the top level route handlers.
362 #
363 # Here an example to create a simple web app with Popcorn:
364 #
365 # ~~~
366 # import popcorn
367 #
368 # class HelloHandler
369 # super Handler
370 #
371 # redef fun get(req, res) do res.html "<h1>Hello World!</h1>"
372 # end
373 #
374 # var app = new App
375 # app.use("/", new HelloHandler)
376 # # app.listen("localhost", 3000)
377 # ~~~
378 #
379 # The Popcorn app listens on port 3000 for connections.
380 # The app responds with "Hello World!" for request to the root URL (`/`) or **route**.
381 # For every other path, it will respond with a **404 Not Found**.
382 #
383 # The `req` (request) and `res` (response) parameters are the same that nitcorn provides
384 # so you can do anything else you would do in your route without Popcorn involved.
385 #
386 # Run the app with the following command:
387 #
388 # ~~~bash
389 # nitc app.nit && ./app
390 # ~~~
391 #
392 # Then, load [http://localhost:3000](http://localhost:3000) in a browser to see the output.
393 class App
394 super Router
395 end
396
397 redef class HttpResponse
398
399 # Was this request sent by a handler?
400 var sent = false
401
402 private fun check_sent do
403 if sent then print "Warning: Headers already sent!"
404 end
405
406 # Write data in body response and send it.
407 fun send(raw_data: nullable Writable, status: nullable Int) do
408 if raw_data != null then
409 body += raw_data.write_to_string
410 end
411 if status != null then
412 status_code = status
413 else
414 status_code = 200
415 end
416 check_sent
417 sent = true
418 end
419
420 # Write data as HTML and set the right content type header.
421 fun html(html: nullable Writable, status: nullable Int) do
422 header["Content-Type"] = media_types["html"].as(not null)
423 send(html, status)
424 end
425
426 # Write data as JSON and set the right content type header.
427 fun json(json: nullable Jsonable, status: nullable Int) do
428 header["Content-Type"] = media_types["json"].as(not null)
429 if json == null then
430 send(null, status)
431 else
432 send(json.to_json, status)
433 end
434 end
435
436 # Redirect response to `location`
437 fun redirect(location: String, status: nullable Int) do
438 header["Location"] = location
439 if status != null then
440 status_code = status
441 else
442 status_code = 302
443 end
444 check_sent
445 sent = true
446 end
447
448 # TODO The error message should be parameterizable.
449 fun error(status: Int) do
450 html("Error", status)
451 end
452 end