From 6bca2e642b64ff8d4c66e425e4ea266641e86c5d Mon Sep 17 00:00:00 2001 From: Colin Okay Date: Tue, 8 Feb 2022 12:33:42 -0600 Subject: updated readme --- README.md | 219 +++++++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 138 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 799bdcc..1c3d386 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,158 @@ # What -`lazybones` is a half-hearted effort at a small HTTP route handling -framework for smallish dynamic sites, preferably those whose state -lives in the lisp process. +`lazybones` is a small HTTP routing framework. -It is not at all 'field tested', does not compete with more thorougly -conceived frameworks, and is itself just a kind of wrapper around -clack. +Features include: -I use lazybones for personal projects. Here are a few: +- 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. -- [Time Well Spent](https://github.com/cbeo/tws) is a time tracker with [EBS](https://www.joelonsoftware.com/2007/10/26/evidence-based-scheduling/) features. -- [Compost](https://github.com/cbeo/compost) a message board in a single deployable binary, I use it for small group collaborations. +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. -# Basic Example -``` lisp -(defpackage #:hello-site - (:use #:cl) - (import-from #:lazybones - #:defroute - #:http-ok - #:http-err) - (import-from #:fictitious-image-db-package - #:get-image-ids - #:lookup-image - #:image-mimetype - #:image-data)) - -(in-package :hello-site) - -(defroute :get "/hello" - (http-ok "text/html" - (spinneret:with-html-string - (:doctype) - (:html :lang "en" - (:head - (:meta :charset "utf-8") - (:title "some images")) - (:body - (:h1 "Hello") - (:ul - (dolist (img-id (get-image-ids)) ; assuming some function to get ids - (:li (:img :src (format nil "/image/~a" img-id)))))))))) - -(defroute :get "/image/:id" - (alexandria:if-let (found (lookup-image id)) ; assuming a funciton to lookup images - (http-ok (image-mimetype found) ; and to query properties - (image-data found)) ; a byte vector - (http-err 404 "Not Found"))) - - -(start :port 5000) ; start the server - -``` - -Example code that would serve a page containing a list of images. +## Main components -## Helpful Features +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. -Inside of any handler there is a dynamically bound variable `*req*` -that holds the HTTP request being processed. -Using a form called `with-handler-preamble`, groups of handlers can be -defined that all perform some initialization / access control steps. +## Example -For example: +The following is quick example showing a few things that `lazybones` can do. ``` lisp +(asdf:load-system "lazybones-hunchentoot") -(with-handler-preamble - ((unless (authorized-request *req*) - (http-err 401 "Unauthorized")) - - (make-database-connection)) - - (defroute :post "/image/:id/edit" - ;; ... handle image post ... - ) - - (defroute :delete "/image/:id" - ;; ... handle image delete ... - )) +(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))) ``` -# Installation - -If you insist on trying `lazybones` for yourself, you'll need to -ensure that your quicklisp can find it. - -The easiest approach is probably something like: +## Backends - cd ~/quicklisp/local-projects/ - git clone https://github.com/cbeo/lazybones - -With those packages available to quicklisp, you should be able to do -`(ql:quickload :lazybones)` in your REPL. +**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. -- cgit v1.2.3