lib/popcorn: introduce pop_handlers
authorAlexandre Terrasa <alexandre@moz-code.org>
Mon, 16 May 2016 15:44:26 +0000 (11:44 -0400)
committerAlexandre Terrasa <alexandre@moz-code.org>
Thu, 19 May 2016 21:33:52 +0000 (17:33 -0400)
Signed-off-by: Alexandre Terrasa <alexandre@moz-code.org>

lib/popcorn/pop_handlers.nit [new file with mode: 0644]

diff --git a/lib/popcorn/pop_handlers.nit b/lib/popcorn/pop_handlers.nit
new file mode 100644 (file)
index 0000000..2c85e15
--- /dev/null
@@ -0,0 +1,428 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Route handlers.
+module pop_handlers
+
+import pop_routes
+import json
+
+# Class handler for a route.
+#
+# **Routing** refers to determining how an application responds to a client request
+# to a particular endpoint, which is a URI (or path) and a specific HTTP request
+# method GET, POST, PUT or DELETE (other methods are not suported yet).
+#
+# Each route can have one or more handler methods, which are executed when the route is matched.
+#
+# Route handlers definition takes the following form:
+#
+# ~~~nitish
+# class MyHandler
+#      super Handler
+#
+#      redef fun METHOD(req, res) do end
+# end
+# ~~~
+#
+# Where:
+# * `MyHandler` is the name of the handler you will add to the app.
+# * `METHOD` can be replaced by `get`, `post`, `put` or `delete`.
+#
+# The following example responds with `Hello World!` to GET and POST requests:
+#
+# ~~~
+# class MyHandler
+#      super Handler
+#
+#      redef fun get(req, res) do res.send "Got a GET request"
+#      redef fun post(req, res) do res.send "Got a POST request"
+# end
+# ~~~
+#
+# To make your handler responds to a specific route, you have to add it to the app.
+#
+# Respond to POST request on the root route (`/`), the application's home page:
+#
+# ~~~
+# var app = new App
+# app.use("/", new MyHandler)
+# ~~~
+#
+# Respond to a request to the `/user` route:
+#
+# ~~~
+# app.use("/user", new MyHandler)
+# ~~~
+abstract class Handler
+
+       # Call `all(req, res)` if `route` matches `uri`.
+       private fun handle(route: AppRoute, uri: String, req: HttpRequest, res: HttpResponse) do
+               if route.match(uri) then
+                       if route isa AppParamRoute then
+                               req.uri_params = route.parse_uri_parameters(uri)
+                       end
+                       all(req, res)
+               end
+       end
+
+       # Handler to all kind of HTTP request methods.
+       #
+       # `all` is a special request handler, which is not derived from any
+       # HTTP method. This method is used to respond at a path for all request methods.
+       #
+       # In the following example, the handler will be executed for requests to "/user"
+       # whether you are using GET, POST, PUT, DELETE, or any other HTTP request method.
+       #
+       # ~~~
+       # class AllHandler
+       #       super Handler
+       #
+       #       redef fun all(req, res) do res.send "Every request to the homepage"
+       # end
+       # ~~~
+       #
+       # Using the `all` method you can also implement other HTTP request methods.
+       #
+       # ~~~
+       # class MergeHandler
+       #       super Handler
+       #
+       #       redef fun all(req, res) do
+       #               if req.method == "MERGE" then
+       #                       # handle that method
+       #               else super # keep handle GET, POST, PUT and DELETE methods
+       #       end
+       # end
+       # ~~~
+       fun all(req: HttpRequest, res: HttpResponse) do
+               if req.method == "GET" then
+                       get(req, res)
+               else if req.method == "POST" then
+                       post(req, res)
+               else if req.method == "PUT" then
+                       put(req, res)
+               else if req.method == "DELETE" then
+                       delete(req, res)
+               else
+                       res.status_code = 405
+               end
+       end
+
+       # GET handler.
+       #
+       # Exemple of route responding to GET requests.
+       # ~~~
+       # class GetHandler
+       #       super Handler
+       #
+       #       redef fun get(req, res) do res.send "GETrequest received"
+       # end
+       # ~~~
+       fun get(req: HttpRequest, res: HttpResponse) do end
+
+       # POST handler.
+       #
+       # Exemple of route responding to POST requests.
+       # ~~~
+       # class PostHandler
+       #       super Handler
+       #
+       #       redef fun post(req, res) do res.send "POST request received"
+       # end
+       # ~~~
+       fun post(req: HttpRequest, res: HttpResponse) do end
+
+       # PUT handler.
+       #
+       # Exemple of route responding to PUT requests.
+       # ~~~
+       # class PutHandler
+       #       super Handler
+       #
+       #       redef fun put(req, res) do res.send "PUT request received"
+       # end
+       # ~~~
+       fun put(req: HttpRequest, res: HttpResponse) do end
+
+       # DELETE handler.
+       #
+       # Exemple of route responding to PUT requests.
+       # ~~~
+       # class DeleteHandler
+       #       super Handler
+       #
+       #       redef fun delete(req, res) do res.send "DELETE request received"
+       # end
+       # ~~~
+       fun delete(req: HttpRequest, res: HttpResponse) do end
+end
+
+# Static files server.
+#
+# To serve static files such as images, CSS files, and JavaScript files, use the
+# Popcorn built-in handler `StaticHandler`.
+#
+# Pass the name of the directory that contains the static assets to the StaticHandler
+# init method to start serving the files directly.
+# For example, use the following code to serve images, CSS files, and JavaScript files
+# in a directory named `public`:
+#
+# ~~~
+# var app = new App
+# app.use("/", new StaticHandler("public/"))
+# ~~~
+#
+# Now, you can load the files that are in the `public` directory:
+#
+# ~~~raw
+# http://localhost:3000/images/trollface.jpg
+# http://localhost:3000/css/style.css
+# http://localhost:3000/js/app.js
+# http://localhost:3000/hello.html
+# ~~~
+#
+# Popcorn looks up the files relative to the static directory, so the name of the
+# static directory is not part of the URL.
+# To use multiple static assets directories, add the `StaticHandler` multiple times:
+#
+# ~~~
+# app.use("/", new StaticHandler("public/"))
+# app.use("/", new StaticHandler("files/"))
+# ~~~
+#
+# Popcorn looks up the files in the order in which you set the static directories
+# with the `use` method.
+#
+# To create a virtual path prefix (where the path does not actually exist in the file system)
+# for files that are served by the `StaticHandler`, specify a mount path for the
+# static directory, as shown below:
+#
+# ~~~
+# app.use("/static/", new StaticHandler("public/"))
+# ~~~
+#
+# Now, you can load the files that are in the public directory from the `/static`
+# path prefix.
+#
+# ~~~raw
+# http://localhost:3000/static/images/trollface.jpg
+# http://localhost:3000/static/css/style.css
+# http://localhost:3000/static/js/app.js
+# http://localhost:3000/static/hello.html
+# ~~~
+#
+# However, the path that you provide to the `StaticHandler` is relative to the
+# directory from where you launch your app.
+# If you run the app from another directory, it’s safer to use the absolute path of
+# the directory that you want to serve.
+class StaticHandler
+       super Handler
+
+       # Static files directory to serve.
+       var static_dir: String
+
+       # Internal file server used to lookup and render files.
+       var file_server: FileServer is lazy do
+               var srv = new FileServer(static_dir)
+               srv.show_directory_listing = false
+               return srv
+       end
+
+       redef fun handle(route, uri, req, res) do
+               var answer = file_server.answer(req, route.uri_root(uri))
+               if answer.status_code == 200 then
+                       res.status_code = answer.status_code
+                       res.header.add_all answer.header
+                       res.files.add_all answer.files
+                       res.send
+               else if answer.status_code != 404 then
+                       res.status_code = answer.status_code
+               end
+       end
+end
+
+# Mountable routers
+#
+# Use the `Router` class to create modular, mountable route handlers.
+# A Router instance is a complete middleware and routing system; for this reason,
+# it is often referred to as a “mini-app”.
+#
+# The following example creates a router as a module, loads a middleware handler in it,
+# defines some routes, and mounts the router module on a path in the main app.
+#
+# ~~~
+# class AppHome
+#      super Handler
+#
+#      redef fun get(req, res) do res.send "Site Home"
+# end
+#
+# class UserLogger
+#      super Handler
+#
+#      redef fun all(req, res) do print "User logged"
+# end
+#
+# class UserHome
+#      super Handler
+#
+#      redef fun get(req, res) do res.send "User Home"
+# end
+#
+# class UserProfile
+#      super Handler
+#
+#      redef fun get(req, res) do res.send "User Profile"
+# end
+#
+# var user_router = new Router
+# user_router.use("/*", new UserLogger)
+# user_router.use("/", new UserHome)
+# user_router.use("/profile", new UserProfile)
+#
+# var app = new App
+# app.use("/", new AppHome)
+# app.use("/user", user_router)
+# ~~~
+#
+# The app will now be able to handle requests to /user and /user/profile, as well
+# as call the `Time` middleware handler that is specific to the route.
+class Router
+       super Handler
+
+       # List of handlers to match with requests.
+       private var handlers = new Map[AppRoute, Handler]
+
+       # Register a `handler` for a route `path`.
+       #
+       # Route paths are matched in registration order.
+       fun use(path: String, handler: Handler) do
+               var route
+               if handler isa Router or handler isa StaticHandler then
+                       route = new AppGlobRoute(path)
+               else if path.has_suffix("*") then
+                       route = new AppGlobRoute(path)
+               else
+                       route = new AppParamRoute(path)
+               end
+               handlers[route] = handler
+       end
+
+       redef fun handle(route, uri, req, res) do
+               if not route.match(uri) then return
+               for hroute, handler in handlers do
+                       handler.handle(hroute, route.uri_root(uri), req, res)
+                       if res.sent then break
+               end
+       end
+end
+
+# Popcorn application.
+#
+# The `App`  is the main point of the application.
+# It acts as a `Router` that holds the top level route handlers.
+#
+# Here an example to create a simple web app with Popcorn:
+#
+# ~~~
+# import popcorn
+#
+# class HelloHandler
+#      super Handler
+#
+#      redef fun get(req, res) do res.html "<h1>Hello World!</h1>"
+# end
+#
+# var app = new App
+# app.use("/", new HelloHandler)
+# # app.listen("localhost", 3000)
+# ~~~
+#
+# The Popcorn app listens on port 3000 for connections.
+# The app responds with "Hello World!" for request to the root URL (`/`) or **route**.
+# For every other path, it will respond with a **404 Not Found**.
+#
+# The `req` (request) and `res` (response) parameters are the same that nitcorn provides
+# so you can do anything else you would do in your route without Popcorn involved.
+#
+# Run the app with the following command:
+#
+# ~~~bash
+# nitc app.nit && ./app
+# ~~~
+#
+# Then, load [http://localhost:3000](http://localhost:3000) in a browser to see the output.
+class App
+       super Router
+end
+
+redef class HttpResponse
+
+       # Was this request sent by a handler?
+       var sent = false
+
+       private fun check_sent do
+               if sent then print "Warning: Headers already sent!"
+       end
+
+       # Write data in body response and send it.
+       fun send(raw_data: nullable Writable, status: nullable Int) do
+               if raw_data != null then
+                       body += raw_data.write_to_string
+               end
+               if status != null then
+                       status_code = status
+               else
+                       status_code = 200
+               end
+               check_sent
+               sent = true
+       end
+
+       # Write data as HTML and set the right content type header.
+       fun html(html: nullable Writable, status: nullable Int) do
+               header["Content-Type"] = media_types["html"].as(not null)
+               send(html, status)
+       end
+
+       # Write data as JSON and set the right content type header.
+       fun json(json: nullable Jsonable, status: nullable Int) do
+               header["Content-Type"] = media_types["json"].as(not null)
+               if json == null then
+                       send(null, status)
+               else
+                       send(json.to_json, status)
+               end
+       end
+
+       # Redirect response to `location`
+       fun redirect(location: String, status: nullable Int) do
+               header["Location"] = location
+               if status != null then
+                       status_code = status
+               else
+                       status_code = 302
+               end
+               check_sent
+               sent = true
+       end
+
+       # TODO The error message should be parameterizable.
+       fun error(status: Int) do
+               html("Error", status)
+       end
+end