(asdf:load-system "lazybones-hunchentoot") (defpackage #:lazybones-example (:use #:cl) (:local-nicknames (#:lzb #:lazybones)) (:import-from #:lazybones #:defendpoint* #:http-err)) (in-package :lazybones-example) ;; First make a server and add some custom error responses (defvar *server* (lzb:create-server :port 8888)) (defun custom-404 () (format nil "~a wasn't found :o~%" (lzb:request-path))) ; can use request functiosn (defun custom-403 () "You, in particular, can't do that. :P ") (defun custom-500 () "Bah. Error.") (lzb:set-canned-response *server* 404 'custom-404 "text/plain" ) (lzb:set-canned-response *server* 403 'custom-403 "text/plain" ) (lzb:set-canned-response *server* 500 'custom-500 "text/plain") ;; 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" :prefix "/eg" :description "Just an API that defines some endpoints. These endpoints aren't meant to accomplish anything. Merely testing 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 (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"))) ;; now we install the app to the server (lzb:install-app *server* (lzb:app)) ; (app) is the default app for this package ;; 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. ;; The general syntax is: DEFENDPOINT* HTTP-METHOD ROUTE-TEMPLATE QUERY-PARAMETERS OPTIONS DOCSTRING BODY ... (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'." (print (lzb:request-body)) ; dummy implementation, prints post body to stdout (lzb:set-response-cookie "testappsession" "coolsessionbro" :path "/" :domain "localhost") "true") ;; The next route defines a route variable WHO (defendpoint* :get "/hello/:who:" () () "Echos back Hello WHO" (format nil "Hello ~a~%" who)) ; use the route variable here (defendpoint* :post "/hello/:who:" () (:auth t) ; use the app's default authorizor "Echo's back 'Hello WHO, I got your message BODY' where BODY is the post body." (print (lzb:request-header :content-type)) (let ((body (lzb:request-body))) (format nil "Hello ~a, I got your message ~a~%" who body))) ;; Some helpers, these are used to parse url variables and query ;; parameters. Their docstrings are used in the API documentation (defun int (string) "An Integer" (parse-integer string)) (defun str (string) "A String" string) ;; In the following, two query parameters are specififed. NAME is ;; meant to be a string and AGE is an integer. If AGE is not an integer, ;; a 500 error will be returned. The syntax is (VAR PARSER) (defendpoint* :get "/search" ((name str) (age int)) () "Echo the search parameters in a nice list." (format nil "Name: ~a~%age: ~a~%" name age)) (defun crapshoot-authorizer () "Randomly decides that the request is authorized" (< 5 (random 10))) (defendpoint* :post "/crapshoot" () (:auth 'crapshoot-authorizer) ; use a custom authorizer "Echos back 'You made it' if the request was authorized" "You made it") ;; Route variables can accept parsers / preformatters ;; these will parse a value and supply it to the argument of the handler. (defendpoint* :get "/random/:lo int:/:hi int:" () () "Echo back a random number between lo and hi" (if (< lo hi) (format nil "The number is: ~a~%" (+ lo (random (- hi lo)))) (http-err 404))) ; Can't find a number X such that LO >= HI and LO < X < HI (defun person-by-id (id) "ID of a person" ;; 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") ; override the app's default content type for HTTP responses "Returns a json representation of the [Person](#person)." (jonathan:to-json person)) (lzb::set-definition "Person" "#person" "An instance of person. As JSON, it looks like: { \"NAME\" : string , \"OCCUPATION\" : string , \"ID\" : integer } ") (defclass animal () ((genus :initarg :genus :documentation "The genus") (species :initarg :species :documentation "The species") (population :initarg :population :documentation "Population on Earth") (habitat :initarg :habitat :documentation "Where the animal lives")) (:documentation "An animal")) ;; add a documentation definition for animal class, the provided slots ;; will show up in the api docs (lzb:add-class-to-definitions (lzb:app) 'animal 'genus 'species 'population 'habitat) (defvar +dummy-animal+ (make-instance 'animal :genus "Pseudocheirus" :species "Peregrinis" :population "Unknown" :habitat "Australia")) ;; you can refer to definitions in docstrings (defendpoint* :get "/animal/:genus:/:species:" () () "Prints information about [Animal](#animal) specified by GENUS and SPECIES" (if (and (string-equal genus "Pseudocheirus") (string-equal species "Peregrinis")) (with-output-to-string (out) (with-slots (genus species population habitat) +dummy-animal+ (format out "The ~a ~a lives in ~a. Population: ~a~%" genus species population habitat))) (http-err 404)))