Routing library taking advantage of the MOP
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
hunchentoot
src
tests
.gitignore
LICENSE
froute.asd
readme.org

readme.org

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

  • SBCL
  • CCL

but should hopefully work on any lisp that supports the Metaobject Protocol.

Installing

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)

Parameters

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)))))

Using CLOS

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%