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