lib/popcorn: create package
authorAlexandre Terrasa <alexandre@moz-code.org>
Mon, 16 May 2016 19:05:34 +0000 (15:05 -0400)
committerAlexandre Terrasa <alexandre@moz-code.org>
Fri, 20 May 2016 00:13:03 +0000 (20:13 -0400)
Signed-off-by: Alexandre Terrasa <alexandre@moz-code.org>

lib/popcorn/.gitignore [new file with mode: 0644]
lib/popcorn/Makefile [new file with mode: 0644]
lib/popcorn/README.md [new file with mode: 0644]
lib/popcorn/package.ini [new file with mode: 0644]

diff --git a/lib/popcorn/.gitignore b/lib/popcorn/.gitignore
new file mode 100644 (file)
index 0000000..c32211a
--- /dev/null
@@ -0,0 +1 @@
+tests/out
diff --git a/lib/popcorn/Makefile b/lib/popcorn/Makefile
new file mode 100644 (file)
index 0000000..12db3df
--- /dev/null
@@ -0,0 +1,24 @@
+# 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.
+
+NITUNIT=../../bin/nitunit
+
+check:
+       $(NITUNIT) README.md
+       $(NITUNIT) pop_routes.nit
+       $(NITUNIT) pop_handlers.nit
+       $(NITUNIT) popcorn.nit
+       cd tests; make check
diff --git a/lib/popcorn/README.md b/lib/popcorn/README.md
new file mode 100644 (file)
index 0000000..08f7c9a
--- /dev/null
@@ -0,0 +1,815 @@
+# Popcorn
+
+**Why endure plain corn when you can pop it?!**
+
+Popcorn is a minimal yet powerful nit web application framework that provides cool
+features for lazy developpers.
+
+Popcorn is built over nitcorn to provide a clean and user friendly interface
+without all the boiler plate code.
+
+## What does it taste like?
+
+Set up is quick and easy as 10 lines of code.
+Create a file `app.nit` and add the following code:
+
+~~~
+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 requests 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.
+
+Here the output using the `curl` command:
+
+~~~bash
+$ curl localhost:3000
+<h1>Hello World!</h1>
+
+$ curl localhost:3000/wrong_uri
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Not Found</title>
+</head>
+<body>
+<h1>404 Not Found</h1>
+</body>
+</html>
+~~~
+
+This is why we love popcorn!
+
+## Basic routing
+
+**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
+import popcorn
+
+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 to GET and POST requests:
+
+~~~
+import popcorn
+
+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)
+~~~
+
+For more details about routing, see the routing section.
+
+## Serving static files with Popcorn
+
+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`:
+
+~~~
+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.
+
+## Advanced Routing
+
+**Routing** refers to the definition of application end points (URIs) and how
+they respond to client requests. For an introduction to routing, see the Basic routing
+section.
+
+The following code is an example of a very basic route.
+
+~~~
+import popcorn
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Hello World!"
+end
+
+var app = new App
+app.use("/", new HelloHandler)
+~~~
+
+### Route methods
+
+A **route method** is derived from one of the HTTP methods, and is attached to an
+instance of the Handler class.
+
+The following code is an example of routes that are defined for the GET and the POST
+methods to the root of the app.
+
+~~~
+import popcorn
+
+class GetPostHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "GET request to the homepage"
+       redef fun post(req, res) do res.send "POST request to the homepage"
+end
+
+var app = new App
+app.use("/", new GetPostHandler)
+~~~
+
+Popcorn supports the following routing methods that correspond to HTTP methods:
+get, post, put, and delete.
+
+The request query string is accessed through the `req` parameter:
+
+~~~
+import popcorn
+import template
+
+class QueryStringHandler
+       super Handler
+
+       redef fun get(req, res) do
+               var tpl = new Template
+               tpl.addn "URI: {req.uri}"
+               tpl.addn "Query string: {req.query_string}"
+               for name, arg in req.get_args do
+                       tpl.addn "{name}: {arg}"
+               end
+        res.send tpl
+       end
+end
+
+var app = new App
+app.use("/", new QueryStringHandler)
+app.listen("localhost", 3000)
+~~~
+
+Post parameters can also be accessed through the `req` parameter:
+
+~~~
+import popcorn
+import template
+
+class PostHandler
+       super Handler
+
+       redef fun post(req, res) do
+               var tpl = new Template
+               tpl.addn "URI: {req.uri}"
+               tpl.addn "Body: {req.body}"
+               for name, arg in req.post_args do
+                       tpl.addn "{name}: {arg}"
+               end
+        res.send tpl
+       end
+end
+
+var app = new App
+app.use("/", new PostHandler)
+app.listen("localhost", 3000)
+~~~
+
+There is a special routing method, `all(res, req)`, 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.
+
+~~~
+import popcorn
+
+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.
+
+~~~
+import popcorn
+
+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
+~~~
+
+### Route paths
+
+**Route paths**, in combination with a request handlers, define the endpoints at
+which requests can be made.
+Route paths can be strings, parameterized strings or glob patterns.
+Query strings such as `?q=foo`are not part of the route path.
+
+Popcorn uses the `Handler::match(uri)` method to match the route paths.
+
+Here are some examples of route paths based on strings.
+
+This route path will match requests to the root route, `/`.
+
+~~~
+import popcorn
+
+class MyHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Got a GET request"
+end
+
+var app = new App
+app.use("/", new MyHandler)
+~~~
+
+This route path will match requests to `/about`.
+
+~~~
+app.use("/about", new MyHandler)
+~~~
+
+This route path will match requests to `/random.text`.
+
+~~~
+app.use("/random.text", new MyHandler)
+~~~
+
+During the query/response process, routes are matched by order of declaration
+through the `App::use` method.
+
+The app declared in this example will try to match the routes in this order:
+
+1. `/`
+2. `/about`
+3. `/random.text`
+
+### Route parameters
+
+**Route parameters** are variable parts of a route path. They can be used to path
+arguments within the URI.\13
+Parameters in a route are prefixed with a colon `:` like in `:userId`, `:year`.
+
+The following example declares a handler `UserHome` that responds with the `user`
+name.
+
+~~~
+import popcorn
+
+class UserHome
+       super Handler
+
+       redef fun get(req, res) do
+               var user = req.param("user")
+               if user != null then
+                       res.send "Hello {user}"
+               else
+                       res.send("Nothing received", 400)
+               end
+       end
+end
+
+var app = new App
+app.use("/:user", new UserHome)
+app.listen("localhost", 3000)
+~~~
+
+The `UserHome` handler listen to every path matching `/:user`. This can be `/Morriar`,
+`/10`, ... but not `/Morriar/profile` since route follow the strict matching rule.
+
+### Glob routes
+
+**Glob routes** are routes that match only on a prefix, thus accepting a wider range
+of URI.
+Glob routes end with the symbol `*`.
+
+Here we define a `UserItem` handler that will respond to any URI matching the prefix
+`/user/:user/item/:item`.
+Note that glob route are compatible with route parameters.
+
+~~~
+import popcorn
+
+class UserItem
+       super Handler
+
+       redef fun get(req, res) do
+               var user = req.param("user")
+               var item = req.param("item")
+               if user == null or item == null then
+                       res.send("Nothing received", 400)
+               else
+                       res.send "Here the item {item} of the use {user}."
+               end
+       end
+end
+
+var app = new App
+app.use("/user/:user/item/:item/*", new UserItem)
+app.listen("localhost", 3000)
+~~~
+
+## Response methods
+
+The methods on the response object (`res`), can is used to manipulate the
+request-response cycle.
+If none of these methods are called from a route handler, the client request will
+receive a `404 Not found` error.
+
+* `res.html()` Send a HTML response.
+* `res.json()` Send a JSON response.
+* `res.redirect()` Redirect a request.
+* `res.send()` Send a response of various types.
+* `res.error()` Set the response status code and send its message as the response body.
+
+## Middlewares
+
+### Overview
+
+**Middleware** handlers are handlers that typically do not send `HttpResponse` responses.
+Middleware handlers can perform the following tasks:
+
+* Execute any code.
+* Make changes to the request and the response objects.
+* End its action and pass to the next handler in the stack.
+
+If a middleware handler makes a call to `res.send()`, it provoques the end the
+request-response cycle and the response is sent to the client.
+
+### Ultra simple logger example
+
+Here is an example of a simple “Hello World” Popcorn application.
+We add a middleware handler to the application called MyLogger that prints a simple
+log message in the app stdout.
+
+~~~
+import popcorn
+
+class MyLogger
+       super Handler
+
+       redef fun all(req, res) do print "Request Logged!"
+end
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Hello World!"
+end
+
+
+var app = new App
+app.use("/*", new MyLogger)
+app.use("/", new HelloHandler)
+app.listen("localhost", 3000)
+~~~
+
+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`.
+
+### Ultra cool, more advanced logger example
+
+Next, we’ll create a middleware handler called “LogHandler” that prints the requested
+uri, the response status and the time it took to Popcorn to process the request.
+
+This example gives a simplified version of the `RequestClock` and `ConsoleLog` middlewares.
+
+~~~
+import popcorn
+import realtime
+
+redef class HttpRequest
+       # Time that request was received by the Popcorn app.
+       var timer: nullable Clock = null
+end
+
+class RequestTimeHandler
+       super Handler
+
+       redef fun all(req, res) do req.timer = new Clock
+end
+
+class LogHandler
+       super Handler
+
+       redef fun all(req, res) do
+               var timer = req.timer
+               if timer != null then
+                       print "{req.method} {req.uri} {res.color_status} ({timer.total})"
+               else
+                       print "{req.method} {req.uri} {res.color_status}"
+               end
+       end
+end
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Hello World!"
+end
+
+var app = new App
+app.use("/*", new RequestTimeHandler)
+app.use("/", new HelloHandler)
+app.use("/*", new LogHandler)
+app.listen("localhost", 3000)
+~~~
+
+First, we attach a new attribute `timer` to every `HttpRequest`.
+Doing so we can access our data from all handlers that import our module, directly
+from the `req` parameter.
+
+We use the new middleware called `RequestTimeHandler` to initialize the request timer.
+
+Finally, our `LogHandler` will display a bunch of data and use the request `timer`
+to display the time it took to process the request.
+
+The app now uses the `RequestTimeHandler` middleware for every requests received
+by the Popcorn app.
+The page is processed the `HelloHandler` to display the index page.
+And, before every response is sent, the `LogHandler` is activated to display the
+log line.
+
+Because you have access to the request object, the response object, and all the
+Popcorn API, the possibilities with middleware functions are endless.
+
+### Built-in middlewares
+
+Starting with version 0.1, Popcorn provide a set of built-in middleware that can
+be used to develop your app faster.
+
+* `RequestClock`: initializes requests clock.
+* `ConsoleLog`: displays resquest and response status in console (can be used with `RequestClock`).
+* `SessionInit`: initializes requests session (see the `Sessions` section).
+* `StaticServer`: serves static files (see the `Serving static files with Popcorn` section).
+* `Router`: a mountable mini-app (see the `Mountable routers` section).
+
+## 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.
+
+~~~
+import popcorn
+
+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)
+app.listen("localhost", 3000)
+~~~
+
+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.
+
+## Error handling
+
+**Error handling** is based on middleware handlers.
+
+Define error-handling middlewares in the same way as other middleware handlers:
+
+~~~
+import popcorn
+
+class SimpleErrorHandler
+       super Handler
+
+       redef fun all(req, res) do
+               if res.status_code != 200 then
+                       print "An error occurred! {res.status_code})"
+               end
+       end
+end
+
+class HelloHandler
+       super Handler
+
+       redef fun get(req, res) do res.send "Hello World!"
+end
+
+var app = new App
+app.use("/", new HelloHandler)
+app.use("/*", new SimpleErrorHandler)
+app.listen("localhost", 3000)
+~~~
+
+In this example, every non-200 response is caught by the `SimpleErrorHandler`
+that print an error in stdout.
+
+By defining multiple middleware error handlers, you can take multiple action depending
+on the kind of error or the kind of interface you provide (HTML, XML, JSON...).
+
+Here an example of the 404 custom error page in HTML:
+
+~~~
+import popcorn
+import template
+
+class HtmlErrorTemplate
+       super Template
+
+       var status: Int
+       var message: nullable String
+
+       redef fun rendering do add """
+               <!DOCTYPE html>
+               <html>
+               <head>
+                       <meta charset="utf-8">
+                       <title>{{{message or else status}}}</title>
+               </head>
+               <body>
+               <h1>{{{status}}} {{{message or else ""}}}</h1>
+               </body>
+               </html>"""
+end
+
+class HtmlErrorHandler
+       super Handler
+
+       redef fun all(req, res) do
+               if res.status_code != 200 then
+                       res.send(new HtmlErrorTemplate(res.status_code, "An error occurred!"))
+               end
+       end
+end
+
+var app = new App
+app.use("/*", new HtmlErrorHandler)
+app.listen("localhost", 3000)
+~~~
+
+## Sessions
+
+**Sessions** can be used thanks to the built-in `SessionMiddleware`.
+
+Here a simple example of login button that define a value in the `req` session.
+
+~~~
+import popcorn
+
+redef class Session
+       var is_logged = false
+end
+
+class AppLogin
+       super Handler
+
+       redef fun get(req, res) do
+               res.html """
+               <p>Is logged: {{{req.session.as(not null).is_logged}}}</p>
+               <form action="/" method="POST">
+                       <input type="submit" value="Login" />
+               </form>"""
+       end
+
+       redef fun post(req, res) do
+               req.session.as(not null).is_logged = true
+               res.redirect("/")
+       end
+end
+
+var app = new App
+app.use("/*", new SessionInit)
+app.use("/", new AppLogin)
+app.listen("localhost", 3000)
+~~~
+
+Notice the use of the `SessionInit` on the `/*` route. You must use the
+`SessionInit` first to initialize the request session.
+Without that, your request session will be set to `null`.
+If you don't use sessions in your app, you do not need to include that middleware.
+
+## Database integration
+
+### Mongo DB
+
+If you want to persist your data, Popcorn works well with MongoDB.
+
+In this example, we will show how to store and list user with a Mongo database.
+
+First let's define a handler that access the database to list all the user.
+The mongo database reference is passed to the UserList handler through the `db` attribute.
+
+Then we define a handler that displays the user creation form on GET requests.
+POST requests are used to save the user data.
+
+~~~
+import popcorn
+import mongodb
+import template
+
+class UserList
+       super Handler
+
+       var db: MongoDb
+
+       redef fun get(req, res) do
+               var users = db.collection("users").find_all(new JsonObject)
+
+               var tpl = new Template
+               tpl.add "<h1>Users</h1>"
+               tpl.add "<table>"
+               for user in users do
+                       tpl.add """<tr>
+                               <td>{{{user["login"] or else "null"}}}</td>
+                               <td>{{{user["password"] or else "null"}}}</td>
+                       </tr>"""
+               end
+               tpl.add "</table>"
+               res.html tpl
+       end
+end
+
+class UserForm
+       super Handler
+
+       var db: MongoDb
+
+       redef fun get(req, res) do
+               var tpl = new Template
+               tpl.add """<h1>Add a new user</h1>
+               <form action="/new" method="POST">
+                       <input type="text" name="login" />
+                       <input type="password" name="password" />
+                       <input type="submit" value="save" />
+               </form>"""
+               res.html tpl
+       end
+
+       redef fun post(req, res) do
+               var json = new JsonObject
+               json["login"] = req.post_args["login"]
+               json["password"] = req.post_args["password"]
+               db.collection("users").insert(json)
+               res.redirect "/"
+       end
+end
+
+var mongo = new MongoClient("mongodb://localhost:27017/")
+var db = mongo.database("mongo_example")
+
+var app = new App
+app.use("/", new UserList(db))
+app.use("/new", new UserForm(db))
+app.listen("localhost", 3000)
+~~~
+
+## Angular.JS integration
+
+Loving [AngularJS](https://angularjs.org/)? Popcorn is made for Angular and for you!
+
+Using the StaticHandler with a glob route, you can easily redirect all HTTP requests
+to your angular controller:
+
+~~~
+import popcorn
+
+var app = new App
+app.use("/*", new StaticHandler("my-ng-app/"))
+app.listen("localhost", 3000)
+~~~
+
+See the examples for a more detailed use case working with a JSON API.
diff --git a/lib/popcorn/package.ini b/lib/popcorn/package.ini
new file mode 100644 (file)
index 0000000..8f99b9c
--- /dev/null
@@ -0,0 +1,12 @@
+[package]
+name=popcorn
+tags=web,lib
+maintainer=Alexandre Terrasa <alexandre@moz-code.org>
+license=Apache-2.0
+version=1.0
+[upstream]
+browse=https://github.com/nitlang/nit/tree/master/lib/popcorn/
+git=https://github.com/nitlang/nit.git
+git.directory=lib/popcorn/
+homepage=http://nitlanguage.org
+issues=https://github.com/nitlang/nit/issues