Froute - An Http routing class that takes advantage of the MOP
Froute is an Http routing metaclass that uses the Metaobject Protocol that lets you take advantage of CLOS to flexibly build up your Http routes.
Currently works with Hunchentoot, but it is designed to be straightforward to work with other web servers, once the adaptors are developed.
Froute has been tested on
but should hopefully work on any lisp that supports the Metaobject Protocol.
Load froute.asd and then run
(ql:quickload 'froute) (ql:quickload 'froute/hunchentoot)
If you want to test the library
(ql:quickload 'froute/test) (asdf:operate 'asdf:test-op 'froute)
Defining a route
To define a route, define a class with a froute-class metaclass and include a :route property to indicate the path the route is invoked with.
(defclass shporgle () () (:metaclass froute:froute-class) (:route "/shporgle"))
When the route is invoked a run method is called with an instance of this class together with the method.
(defmethod froute:run ((r shporgle) (method (eql :get))) (setf (hunchentoot:content-type*) "text/html") "<html><h1>Shporgle!</h1></html>")
An acceptor is provided to interface between Froute and Hunchentoot. To set it up call the following :
(defvar *app* nil) (defun start-server () (setf *app* (make-instance 'froute-hunchentoot:froute-acceptor :port 4343)) (hunchentoot:start *app*)) ;; Start the server (start-server)
You can insert parameters into the route. The values of the parameters will be inserted into the slots of the invoked class.
(defclass goblins () ((id :reader goblin-id)) (:metaclass froute:froute-class) (:route "/goblin/:id")) (defmethod froute:run ((g goblins) method) (setf (hunchentoot:content-type*) "text/html") (format nil "<h1>I am goblin ~A</h1>" (goblin-id g)))
% curl localhost:4343/goblin/groove <h1>I am goblin groove</h1>%
If you want the parameter to consume the rest of the route, append it with an asterix. This is useful for when you want to create the handler for your static files :
(defclass static-handler () ((path :accessor handler-path)) (:metaclass froute:froute-class) (:route "/public/:path*")) (defmethod froute:run ((r static-handler) (method (eql :get))) (hunchentoot:handle-static-file (resource-path (format nil "assets/~A" (handler-path r)))))
You can use inheritance to build up your route :
(defclass api () ((api :reader api)) (:metaclass froute:froute-class) (:route "/api/:api")) (defclass norgle (api) ((id :reader norgle-id)) (:metaclass froute:froute-class) (:route "/norgle/:id")) (defmethod froute:run ((n norgle) method) (format nil "Norgle ~A from api ~A" (norgle-id n) (api n)))
% curl localhost:4343/api/onk/norgle/splorge Norgle splorge from api onk
Note that if you have a slot with the same name as a slot in a class you inherit from, they are the same slot. So you should ensure you keep your slot name distinct.
You can inherit from classes that do not have `froute-class` as a metaclass. They do not affect the route, but you can use these classes to wrap the request handling.
Say you had a page that required authentication headers.
(defclass require-authentication () ()) (defmethod run :around ((r require-authentication) method) (multiple-value-bind (user password) (hunchentoot:authorization) (if (and (string= user "headgoblin") (string= password "s3cr3t")) (call-next-method) "Access Denied"))) (defclass goblins (require-authentication) () (:metaclass froute:froute-class) (:route "/goblins")) (defmethod run ((r goblins) method) "Hurrah")
When the route inherits from `require-authentication` it implicitly requires the authentication check before it will be invoked.
% curl localhost:4343/goblins Access Denied% % curl --user headgoblin:s3cr3t localhost:4343/goblins Hurrah%