Home

Awesome

EASY-ROUTES

EASY-ROUTES is yet another routes handling system on top of Hunchentoot.

It's just glue code for Restas routing subsystem (CL-ROUTES).

It supports:

Usage

Use routes-acceptor acceptor:

(hunchentoot:start (make-instance 'easy-routes:routes-acceptor))

Note that the routes-acceptor returns with HTTP not found if no route matches and doesn't fallback to easy-handlers, and so it doesn't iterate over Hunchentoot *dispatch-table*. Most of the time, that iteration is a useful thing, so you may want to start the easy-routes:easy-routes-acceptor instead, that inherits from Hunchentoot easy-acceptor and so it iterates the dispatch table if no route matches (useful for being able to use define-easy-handler and also handling static files).

Breaking changes

Routes

Syntax


(defroute <name> (<path> &rest <route-options>) <route-params> 
   &body body)
   

with:

Example route

(defroute foo ("/foo/:arg1/:arg2" :method :get
                                  :decorators (@auth @db @html))
   (&get w)
    (format nil "<h1>FOO arg1: ~a arg2: ~a ~a</h1>" arg1 arg2 w))

Url generation

Use genurl function with the name of the route and route parameters as keyword arguments to generate urls.

Example:

(genurl 'save-payment-form :id (id application))

Decorators

Decorators are functions that are executed before the route body. They should call the next parameter function to continue executing the decoration chain and the route body finally.

Examples

(defun @auth (next)
  (let ((*user* (hunchentoot:session-value 'user)))
    (if (not *user*)
	(hunchentoot:redirect "/login")
	(funcall next))))

(defun @html (next)
  (setf (hunchentoot:content-type*) "text/html")
  (funcall next))

(defun @json (next)
  (setf (hunchentoot:content-type*) "application/json")
  (funcall next))

(defun @db (next)
  (postmodern:with-connection *db-spec*
    (funcall next)))

Decorators also support parameters, like in the @check and @check-permission decorators:

(defun @check (next predicate http-error)
  (if (funcall predicate)
      (funcall next)
      (http-error http-error)))

(defun @check-permission (next predicate)
  (if (funcall predicate)
      (funcall next)
      (permission-denied-error)))

Then you can use those decorators passing the needed parameters. predicate and http-error for @check, and predicate for check permission:

(defroute my-protected-route ("/foo" :method :get
                                     :decorators ((@check my-permissions-checking-function hunchentoot:+http-forbidden+)))
   ()			      
   ...)

List of included decorators

@content-type, @html, @json, @headers-out, @header-out, @accept, @cors, @check, @check-permission.

Routes for individual acceptors

By default routes are registered globally in *ROUTES* and *ROUTES-MAPPER* variables. That is convenient for the most common case of running a single EASY-ROUTES service per Lisp image.

But it gets problematic if you want to run several EASY-ROUTES based services on the same Lisp image. If routes are registered globally, then all your acceptors use the same routes and mapper; that means that a service A would also respond to routes defined in service B; that's clearly not what you want.

For that case, you can use acceptor names to define routes for a specific acceptor.

First you need to give your acceptor a name, using :name acceptor parameter:

(hunchentoot:start (make-instance 'easy-routes:routes-acceptor :name 'my-service))

Then, use that name in routes definition :acceptor-name:

(defroute my-route ("/my-route" :acceptor-name my-service)
  ...
)

Now my-route is registered locally to the my-service acceptor; other running EASY-ROUTES acceptors don't have it in their map anymore. That means you can run several EASY-ROUTES acceptors at the same time on the same Lisp image now.

Lastly, you need to use :acceptor-name when generating urls now too:

(genurl 'my-route :acceptor-name 'my-service)

Map of routes visualization

CL-ROUTES package implement special SWANK code for routes map visualization. Just inspect *ROUTES-MAP* variable from your lisp listener.

For example:

#<ROUTES:MAPPER {1007630E53}>
--------------------

Tree of routes
--------------------------------------------------

users invoice-engine::admin/users
api/invoices/chart invoice-engine::invoices-chart-data
invoice-engine::dashboard
logout invoice-engine::logout
company/logo invoice-engine::company-logo
search invoice-engine::global-search
preview-invoice invoice-engine::preview-invoice
dt-invoices invoice-engine::datatables-list-invoices
tenants invoice-engine::admin/tenants
admin/
    settings invoice-engine::admin/settings
    invoice-engine::admin/dashboard
    tenants/new/
        invoice-engine::admin/tenants/create
        invoice-engine::admin/tenants/new
    login/
        invoice-engine::admin/signin
        invoice-engine::admin/login
    tenant/$id invoice-engine::admin/tenant
    users/
        new/
            invoice-engine::admin/users/create
            invoice-engine::admin/users/new
        $id/edit/
            invoice-engine::admin/users/update
            invoice-engine::admin/users/edit
customers/
    invoice-engine::web/list-customers
    $id invoice-engine::view-customer
invoices/
    invoice-engine::list-invoices-route
    $id/
        print invoice-engine::web/print-invoice
        printed invoice-engine::web/printed-invoice
        invoice-engine::view-invoice
        send invoice-engine::web/send-invoice-by-email

Less fancy, but useful too, you can also use (describe easy-routes:*routes-mapper*) to visualize the tree of routes.

Augmented error pages and logs

You can get augmented error pages and logs with request, session and route information, adding easy-routes+errors as dependency, and subclassing from easy-routes-errors-acceptor, like:

(defclass my-acceptor (easy-routes:easy-routes-acceptor easy-routes::easy-routes-errors-acceptor)
  ())

This is implemented with hunchentoots-errors library.

Djula integration

easy-routes+djula system implements support for generating easy-routes urls using route names and arguments in Djula templates (calls genurl function).

Djula template syntax:

{% genurl route-name &rest args %}

Example:

{% genurl my-route :id 22 :foo template-var.key %}

Reference

Functions

@html

(next)

HTML decoration. Sets reply content type to text/html

find-route

(name)

Find a route by name (symbol)

genurl

(route-symbol &rest args &key &allow-other-keys)

Generate a relative url from a route name and arguments

genurl*

(route-symbol &rest args &key &allow-other-keys)

Generate an absolute url from a route name and arguments

redirect

(route-symbol &rest args)

Redirect to a route url. Pass the route name and the parameters.

Macros

defroute

(name template-and-options params &body body)

Route definition syntax

Classes

easy-routes-acceptor

This acceptor tries to match and handle easy-routes first, but fallbacks to easy-routes dispatcher if there's no matching

routes-acceptor

This acceptors handles routes and only routes. If no route is matched then an HTTP NOT FOUND error is returned. If you want to use Hunchentoot easy-handlers dispatch as a fallback, use EASY-ROUTES-ACCEPTOR