# What `lazybones` is a small HTTP routing framework. Features include: - Different server backends. (At the moment only Hunchnetoot is supported) - Modular and "installable" applications. - Handy macros for provisioning apps and defining endpoints. - Livecoding for your endpoint handlers and application confiturations. - Automatic documentation generation for `lazybones:app` instances. Although lazybones can be used to develop and serve page-oriented web sites, it has been written to help me develop "self documenting" HTTP APIs. ## Main components The two main components are the classes `lazybones:app` and `lazybones:endpoint`. Endpoints are objects representing everything needed to handle an HTTP request. Endpoints are collected into groups called apps. Apps, in addition to being collections of endpoints, are the unit of development for larger lazybones projects. Apps can be "installed to" and "uninstalled from" a listening server on the fly. ## Example The following is quick example showing a few things that `lazybones` can do. ``` lisp (asdf:load-system "lazybones-hunchentoot") (defpackage #:lazybones-test (:use #:cl) (:local-nicknames (#:lzb #:lazybones)) (:import-from #:lazybones #:defendpoint* #:http-ok #:http-err)) (in-package :lazybones-test) ;; PPROVISION-APP makes an app. You can supply an optional name, a symbol. ;; In lieu of a supplied name, the name of the package is used as the app's name. (lzb:provision-app () :title "Lazybones Demo App" :version "0.0.0" :description "Just an API that defines some endpoints. These endpoints aren't meant to accomplish anything. merely to test out the lazybones HTTP routing framework." :content-type "text/plain" ; default content type of server responses. :auth 'post-authorizer ; default authorizor for requests that need it 404 'custom-404 ; custom content for error response codes. 403 'custom-403) (defun custom-404 () (format nil "~a wasn't found :(" (lzb:request-path))) (defun custom-403 () "You, in particular, can't do that. :P ") (defun post-authorizer () "Request is authorized if it contains the right TESTAPPSESSION cookie. Obtain such a cookie by posting to the /login endpoint." (string-equal "coolsessionbro" (lzb:request-cookie "testappsession"))) ;; DEFENDPOINT* is a macro to define an endpoint and install it into the ;; app whose name is the current package anme. DEFENDPOINT (without the *) ;; allows you to explictly specify the app where the endpoint is installed. (defendpoint* :post "/login" () "Dummy login endpoint for returning a session cookie. Always returns the \"true\" and sends a set-cookie header, setting 'testappsession' to 'coolsessionbro'." (setf (lzb:response-cookie "testappsession") "coolsessionbro") (http-ok "true")) (defendpoint* :get "/hello/:who:" () "Just says hello to WHO" (http-ok (format nil "Hello ~a" who))) (defendpoint* :post "/hello/:who:" (:auth t) ; use the default authorizor for the app "Post something to hello who" (print (lzb:request-header :content-type)) (let ((body (lzb:request-body))) (http-ok (format nil "Hello ~a, I got your message ~a" who body)))) (defendpoint* :get "/search" () "Echo the search parameters in a nice list." (http-ok (format nil "Query Was:~%~{~a is ~a~%~}~%" (loop for (x . y) in (lzb:request-parameters) collect x collect y)))) (defun crapshoot-authorizer () "Randomly decides that the request is authorized" (< 5 (random 10))) (defendpoint* :post "/search" (:auth 'crapshoot-authorizer) ; use custom authorizer "Echo the search parameters in a nice list, but also has a post-body" (http-ok (with-output-to-string (out) (format out "Query Was:~%~{~a is ~a~%~}~%" (loop for (x . y) in (lzb:request-parameters) collect x collect y)) (terpri) (format out "Decoded Post Body: ~s~%" (lzb:request-body))))) (defun to-int (string) "An Integer" (parse-integer string)) ;; route variables can accept parsers / preformatters ;; these will parse a value and supply it to the argument of the handler. ;; int eh following CATEGORY is an int (defendpoint* :get "/search/:category to-int:" () "Echo the search back, but in a specific category" (assert (typep category 'integer)) ; just to show you. (http-ok (format nil "Searching in ~a with parameters:~%~{~a = ~a~%~}~%" category (loop for (x . y) in (lzb:request-parameters) collect x collect y)))) (defun person-by-id (id) "A Person Instance" ;; The real thing might perform some database operation here. If the ;; operation failed, an error could be signalled, in which case a ;; 500 response would be sent to the client. (list :name "Colin" :occupation "Macrologist" :id (parse-integer id))) (defendpoint* :get "/person/:person person-by-id:" (:content-type "application/json") "Returns a json representation of the person." (http-ok (jonathan:to-json person))) ``` ## Backends **WARNING** Users can mostly ignore thissection. The API for alternate backends is in flux. To implement a new backend for lazybones, consult the `lazybones.backend` package. Define a new system and a new package that `uses` the lazybones.backend package. Implement functions for each symbol. Consult the `lazybones-hunchentoot` system for an example backend.