Merge: Popcorn: use_before, use_after
authorJean Privat <jean@pryen.org>
Thu, 26 May 2016 15:43:35 +0000 (11:43 -0400)
committerJean Privat <jean@pryen.org>
Thu, 26 May 2016 15:43:35 +0000 (11:43 -0400)
This PR introduce two changes in the popcorn request-response cycle:
* Introduce placeholders `use_before` and `use_after` to force handler execution before or after each request. This makes the life of developer easier when using a lot of routers/routers.
* Break response cycle if a between (between as between before and after) handler gives a response then call after_handler. This encourage the use of middleware into `use_before` and `use_after` and avoid requests sent twice. Bonus the api makes more sense like this.

Pull-Request: #2125
Reviewed-by: Jean Privat <jean@pryen.org>

lib/popcorn/README.md
lib/popcorn/examples/middlewares/example_advanced_logger.nit
lib/popcorn/examples/middlewares/example_simple_logger.nit
lib/popcorn/pop_handlers.nit
lib/popcorn/popcorn.nit
lib/popcorn/tests/res/test_example_static_multiple.res
lib/popcorn/tests/test_example_advanced_logger.nit
lib/popcorn/tests/test_example_simple_logger.nit

index 4712faf..a136621 100644 (file)
@@ -428,6 +428,23 @@ receive a `404 Not found` error.
 * `res.send()` Send a response of various types.
 * `res.error()` Set the response status code and send its message as the response body.
 
+## Response cycle
+
+When the popcorn `App` receives a request, the response cycle is the following:
+
+1. `pre-middlewares` lookup matching middlewares registered with `use_before(pre_middleware)`:
+       1. execute matching middleware by registration order
+       2. if a middleware send a response then let the `pre-middlewares` loop continue
+          with the next middleware
+2. `response-handlers` lookup matching handlers registered with `use(handler)`:
+       1. execute matching middleware by registration order
+       2. if a middleware send a response then stop the `response-handlers` loop
+       3. if no hander matches or sends a response, generate a 404 response
+3. `post-middlewares` lookup matching handlers registered with `use_after(post_handler)`:
+       1. execute matching middleware by registration order
+       2. if a middleware send a response then let the `post-middlewares` loop continue
+          with the next middleware
+
 ## Middlewares
 
 ### Overview
@@ -465,7 +482,7 @@ end
 
 
 var app = new App
-app.use("/*", new MyLogger)
+app.use_before("/*", new MyLogger)
 app.use("/", new HelloHandler)
 app.listen("localhost", 3000)
 ~~~
@@ -474,8 +491,9 @@ By using the `MyLogger` handler to the route `/*` we ensure that every requests
 (even 404 ones) pass through the middleware handler.
 This handler just prints “Request Logged!” when a request is received.
 
-The order of middleware loading is important: middleware functions that are loaded first are also executed first.
-In the above example, `MyLogger` will be executed before `HelloHandler`.
+Be default, the order of middleware execution is that are loaded first are also executed first.
+To ensure our middleware `MyLogger` will be executed before all the other, we add it
+with the `use_before` method.
 
 ### Ultra cool, more advanced logger example
 
@@ -519,9 +537,9 @@ class HelloHandler
 end
 
 var app = new App
-app.use("/*", new RequestTimeHandler)
+app.use_before("/*", new RequestTimeHandler)
 app.use("/", new HelloHandler)
-app.use("/*", new LogHandler)
+app.use_after("/*", new LogHandler)
 app.listen("localhost", 3000)
 ~~~
 
@@ -530,9 +548,15 @@ Doing so we can access our data from all handlers that import our module, direct
 from the `req` parameter.
 
 We use the new middleware called `RequestTimeHandler` to initialize the request timer.
+Because of the `use_before` method, the `RequestTimeHandler` middleware will be executed
+before all the others.
+
+We then let the `HelloHandler` produce the response.
 
 Finally, our `LogHandler` will display a bunch of data and use the request `timer`
 to display the time it took to process the request.
+Because of the `use_after` method, the `LogHandler` middleware will be executed after
+all the others.
 
 The app now uses the `RequestTimeHandler` middleware for every requests received
 by the Popcorn app.
index 7f8e3b1..4ca9120 100644 (file)
@@ -48,7 +48,7 @@ class HelloHandler
 end
 
 var app = new App
-app.use("/*", new RequestTimeHandler)
+app.use_before("/*", new RequestTimeHandler)
 app.use("/", new HelloHandler)
-app.use("/*", new LogHandler)
+app.use_after("/*", new LogHandler)
 app.listen("localhost", 3000)
index 98be552..4052169 100644 (file)
@@ -30,6 +30,6 @@ end
 
 
 var app = new App
-app.use("/*", new LogHandler)
+app.use_before("/*", new LogHandler)
 app.use("/", new HelloHandler)
 app.listen("localhost", 3000)
index 2c85e15..1c20b67 100644 (file)
@@ -307,28 +307,71 @@ class Router
        # List of handlers to match with requests.
        private var handlers = new Map[AppRoute, Handler]
 
+       # List of handlers to match before every other.
+       private var pre_handlers = new Map[AppRoute, Handler]
+
+       # List of handlers to match after every other.
+       private var post_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
+               var route = build_route(handler, path)
                handlers[route] = handler
        end
 
+       # Register a pre-handler for a route `path`.
+       #
+       # Prehandlers are matched before every other handlers in registrastion order.
+       fun use_before(path: String, handler: Handler) do
+               var route = build_route(handler, path)
+               pre_handlers[route] = handler
+       end
+
+       # Register a post-handler for a route `path`.
+       #
+       # Posthandlers are matched after every other handlers in registrastion order.
+       fun use_after(path: String, handler: Handler) do
+               var route = build_route(handler, path)
+               post_handlers[route] = handler
+       end
+
        redef fun handle(route, uri, req, res) do
                if not route.match(uri) then return
+               handle_pre(route, uri, req, res)
+               handle_in(route, uri, req, res)
+               handle_post(route, uri, req, res)
+       end
+
+       private fun handle_pre(route: AppRoute, uri: String, req: HttpRequest, res: HttpResponse) do
+               for hroute, handler in pre_handlers do
+                       handler.handle(hroute, route.uri_root(uri), req, res)
+               end
+       end
+
+       private fun handle_in(route: AppRoute, uri: String, req: HttpRequest, res: HttpResponse) do
                for hroute, handler in handlers do
                        handler.handle(hroute, route.uri_root(uri), req, res)
                        if res.sent then break
                end
        end
+
+       private fun handle_post(route: AppRoute, uri: String, req: HttpRequest, res: HttpResponse) do
+               for hroute, handler in post_handlers do
+                       handler.handle(hroute, route.uri_root(uri), req, res)
+               end
+       end
+
+       private fun build_route(handler: Handler, path: String): AppRoute do
+               if handler isa Router or handler isa StaticHandler then
+                       return new AppGlobRoute(path)
+               else if path.has_suffix("*") then
+                       return new AppGlobRoute(path)
+               else
+                       return new AppParamRoute(path)
+               end
+       end
 end
 
 # Popcorn application.
index 887c122..64e4459 100644 (file)
@@ -48,12 +48,19 @@ redef class App
        redef fun answer(req, uri) do
                uri = uri.simplify_path
                var res = new HttpResponse(404)
+               for route, handler in pre_handlers do
+                       handler.handle(route, uri, req, res)
+               end
                for route, handler in handlers do
                        handler.handle(route, uri, req, res)
+                       if res.sent then break
                end
                if not res.sent then
                        res.send(error_tpl(res.status_code, res.status_message), 404)
                end
+               for route, handler in post_handlers do
+                       handler.handle(route, uri, req, res)
+               end
                res.session = req.session
                return res
        end
index 1b14f14..3a6692f 100644 (file)
@@ -28,13 +28,6 @@ alert("Hello World!");
 </html>
 
 [Client] curl -s localhost:*****/
-Warning: Headers already sent!
-<!DOCTYPE html>
-<html>
-       <body>
-               <h1>Another Index</h1>
-       </body>
-</html>
 <!DOCTYPE html>
 <html>
        <body>
index 41193d2..29b3fc2 100644 (file)
@@ -28,9 +28,9 @@ class TestClient
 end
 
 var app = new App
-app.use("/*", new RequestTimeHandler)
+app.use_before("/*", new RequestTimeHandler)
 app.use("/", new HelloHandler)
-app.use("/*", new LogHandler)
+app.use_after("/*", new LogHandler)
 
 var host = test_host
 var port = test_port
index 50f6af9..e8aab41 100644 (file)
@@ -28,7 +28,7 @@ class TestClient
 end
 
 var app = new App
-app.use("/*", new LogHandler)
+app.use_before("/*", new LogHandler)
 app.use("/", new HelloHandler)
 
 var host = test_host