I've been really missing this in LFE. Despite the fact that creating RESTful apps in LFE with YAWS is so simple it doesn't even need a framework, the routes can be a bit difficult to read, due to some of the destructuring that's done (e.g., URL paths).
Take the following route declaration from the yaws-rest-starter project:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(defun routes | |
"Routes for the Volvoshop REST API." | |
;; /order | |
(((list "order") method arg-data) | |
(order-api method arg-data)) | |
;; /order/:id | |
(((list "order" order-id) method arg-data) | |
(order-api method order-id arg-data)) | |
;; /orders | |
(((list "orders") method arg-data) | |
(orders-api method arg-data)) | |
;; /payment/order/:id | |
(((list "payment" "order" order-id) method arg-data) | |
(payment-api method order-id arg-data)) | |
;; When nothing matches, do this | |
((path method arg) | |
(io:format | |
"Unmatched route!~nPath-info: ~p~nmethod: ~p~narg-data: ~p~n~n" | |
(list path method arg)) | |
(lfest-json-resp:not-found "Unmatched route."))) |
Note that this example delegates additional routing to other functions, since all the routing in one function was pretty difficult to read. This, however, detracts from the overall readability in a different way: there's not one place to look to see what functions map to the complete URL-space for this service.
I wanted to be able to do something like what you can do in Clojure:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns volvo-store | |
(:use compojure.core) | |
(:require [compojure.route :as route])) | |
(defroutes handler | |
;; top-level | |
(GET "/" [] | |
"Welcome to the Volvo Store!") | |
;; single order operations | |
(POST "/order" [attrs] | |
(json-response (create-order attrs))) | |
(GET "/order/:id" [id] | |
(json-response (get-order id))) | |
(DELETE "/order/:id" [id] | |
(json-response (delete-order id))) | |
;; order collection operations | |
(GET "/orders" [] | |
(json-response (get-orders))) | |
;; payment operations | |
(GET "/payment/order/:id" [id] | |
(get-payment-status id)) | |
(PUT "/payment/order/:id" [id attrs] | |
(make-payment id attrs)) | |
;; error conditions | |
; XXX not sure how you'd do this in Compojure ... | |
; ('ALLOWONLY | |
; ('GET 'POST 'PUT 'DELETE) | |
; (lfest-json-resp:method-not-allowed)) | |
(route/not-found | |
"Bad path: invalid operation.")) |
LFE is a Lisp with macros, so why not? Also, nox or mheise (I forget who) on the #erlang-lisp channel had previously noted all the lfe* projects (and these too), and suggested that someone create the inevitable "lfest" repo on Github.
Enter lfest. After a weekend of hacking and debugging some tiny macros, surprisingly little code now supports routes definitions like the following in LFE+YAWS web apps:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(defmodule volvo-store | |
(export all)) | |
(include-lib "deps/lfest/include/macros.lfe") | |
(defroutes | |
;; top-level | |
('GET "/" | |
(lfest-html-resp:ok "Welcome to the Volvo Store!")) | |
;; single order operations | |
('POST "/order" | |
(create-order (lfest:get-data arg-data))) | |
('GET "/order/:id" | |
(get-order id)) | |
('PUT "/order/:id" | |
(update-order id (lfest:get-data arg-data))) | |
('DELETE "/order/:id" | |
(delete-order id)) | |
;; order collection operations | |
('GET "/orders" | |
(get-orders)) | |
;; payment operations | |
('GET "/payment/order/:id" | |
(get-payment-status id)) | |
('PUT "/payment/order/:id" | |
(make-payment id (lfest:get-data arg-data))) | |
;; error conditions | |
('ALLOWONLY | |
('GET 'POST 'PUT 'DELETE) | |
(lfest-json-resp:method-not-allowed)) | |
('NOTFOUND | |
(lfest-json-resp:not-found "Bad path: invalid operation."))) |
If you're curious to see what this actually expands to before getting compiled in a .beam file, you can view it here.
Bonus for LFE webdevs: this project also has a module of HTTP status codes, for easy re-use in other projects.
The recent work I've been doing with macros has really helped shape the section I've been planning to write in the LFE User Guide. I haven't wanted it to be yet another dry description of macros in a Lisp-2. Instead, my hope is to help jump-start people into how to think about macros, how to start writing them immediately (without years of study first), and how to debug them.
The trick, as with all complicated subjects, is how to remove the barrier to entry and get folks that necessary hands-on experience without dumbing-down the material. It's almost ready...
Also, it's Bay to Breakers today, and Ocean Beach is bumpin. The synchronicity is just eery. May you party just as hard with routes in LFE.