FileServer
action, which is a standard and minimal file server
HttpRequest
class and services to create it
Serializable::inspect
to show more useful information
more_collections :: more_collections
Highly specific, but useful, collections-related classes.serialization :: serialization_core
Abstract services to serialize Nit objects to different formatscore :: union_find
union–find algorithm using an efficient disjoint-set data structure
# Route handlers.
module pop_handlers
import pop_routes
import csv
# 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
# Default file to serve if nothing matches the request.
#
# `null` for no default file.
var default_file: nullable 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
srv.default_file = default_file
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]
# 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 = 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.
#
# 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
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 CSV and set the right content type header.
fun csv(csv: nullable CsvDocument, status: nullable Int) do
header["Content-Type"] = media_types["csv"].as(not null)
if csv == null then
send(null, status)
else
send(csv.write_to_string, status)
end
end
# Redirect response to `location`
#
# Use by default 303 See Other as it is the RFC
# way to redirect web applications to a new URI.
fun redirect(location: String, status: nullable Int) do
header["Location"] = location
if status != null then
status_code = status
else
status_code = 303
end
check_sent
sent = true
end
# TODO The error message should be parameterizable.
fun error(status: Int) do
html("Error", status)
end
end
lib/popcorn/pop_handlers.nit:17,1--480,3