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