{"id":13804530,"url":"https://github.com/joaotavora/snooze","last_synced_at":"2026-01-24T16:36:16.820Z","repository":{"id":20240206,"uuid":"23512292","full_name":"joaotavora/snooze","owner":"joaotavora","description":"Common Lisp RESTful web development","archived":false,"fork":false,"pushed_at":"2024-06-30T22:23:33.000Z","size":258,"stargazers_count":210,"open_issues_count":22,"forks_count":23,"subscribers_count":12,"default_branch":"master","last_synced_at":"2024-12-25T20:42:02.679Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Common Lisp","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/joaotavora.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2014-08-31T11:40:58.000Z","updated_at":"2024-12-12T19:45:15.000Z","dependencies_parsed_at":"2024-05-02T11:07:30.294Z","dependency_job_id":"7a6243b6-55d3-45dd-a99f-caed0b269b7f","html_url":"https://github.com/joaotavora/snooze","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joaotavora%2Fsnooze","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joaotavora%2Fsnooze/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joaotavora%2Fsnooze/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joaotavora%2Fsnooze/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/joaotavora","download_url":"https://codeload.github.com/joaotavora/snooze/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239225766,"owners_count":19603162,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-04T01:00:49.479Z","updated_at":"2025-10-31T19:30:33.989Z","avatar_url":"https://github.com/joaotavora.png","language":"Common Lisp","funding_links":[],"categories":["REPLs ##","Interfaces to other package managers"],"sub_categories":["Clack plugins"],"readme":"[![Build Status](https://travis-ci.org/joaotavora/snooze.svg?branch=master)](https://travis-ci.org/joaotavora/snooze)\nSnooze\n======\n\n_Snooze_ is an URL router for Common Lisp designed around [REST web\nservices][rest].\n\nAn URL router lets you open URL routes to your application that are\nfriendlier, easier to remember and better supported by other\napplications, such as search engines. RESTful routes are near universal\nin web APIs and [look like this][restful-routes].\n\nAll _Snooze_ does is establish a tight fit between this type of \nroute and plain old Common Lisp. For example, in _Snooze_, routes \nare just functions and HTTP conditions are just Lisp conditions.\n\nSince you stay inside Lisp, if you know how to make a function,\nyou know how to make a route. *There are no regular expressions to\nwrite or extra route-defining syntax to learn*.\n\n_Snooze_ is web-server-backend-agnostic: it can\n[work with any web server](#other-backends).\n\nHere's an example you can try out quickly: a micro-REST service to\nread and write Lisp docstrings over HTTP:\n\n```lisp\n(defpackage #:readme-demo (:use #:cl #:snooze))\n(in-package #:readme-demo)\n\n(defun find-symbol-or-lose (name package)\n  (or (find-symbol (string name) (find-package package))\n      (http-condition 404 \"Sorry, no such symbol\")))\n\n(defroute lispdoc (:get :text/* name \u0026key (package :cl) (doctype 'function))\n  (or (documentation (find-symbol-or-lose name package) doctype)\n      (http-condition 404 \"Sorry, ~a doesn't have any ~a doc\" name doctype)))\n\n(defroute lispdoc (:post :text/plain name \u0026key (package :cl) (doctype 'function))\n  (setf (documentation (find-symbol-or-lose name package) doctype)\n        (payload-as-string)))\n\n;; Let's use clack as a server backend\n(clack:clackup (snooze:make-clack-app) :port 9003)\n```\n\nThis establishes two routes (`GET` for reading and `POST` for writing)\non the URI `localhost:9003/lispdoc/\u003csymbol\u003e`. Here's an illustration\nof how they respond:\n\n\n```HTTP\nGET /lispdoc/defun                         =\u003e 200 OK\nGET /lispdoc/funny-syntax?package=snooze   =\u003e 404 Not found\nGET /lispdoc/in/?valid=args                =\u003e 400 Bad Request\n                                           \nGET /lispdoc/defun                         =\u003e 406 Not Acceptable \nAccept: application/json\n\nPOST /lispdoc/scan?package=cl-ppcre        =\u003e 200 OK \nContent-type: text/plain\n\nPOST /lispdoc/defun                        =\u003e 415 Unsupported Media Type \nContent-type: application/json\n```\n\nThe error codes 400, 406 and 415 are error reporting that you get \"for\nfree\": if the HTTP client strays off these routes, be it for improper\nsyntax or unsupported content types, the correct HTTP condition is\nsignalled.\n\nThe rest of this README contains the [rationale](#rationale) for\nbuilding _Snooze_ and a [tutorial](#tutorial) that builds on the\nsimple application presented above.\n\nStatus\n------\n\nAh, _Snooze_ is kinda *BETA*. The usual disclaimer of warranty\napplies.\n\nRationale\n---------\n\nThere are already some Common Lisp systems for HTTP routing, like\n[caveman][caveman], [cl-rest-server][cl-rest-server],[restas][restas]\nand [ningle][ningle]. Unfortunately, they tend to make you learn some\nextra route-defining syntax. \n\nOn the contrary _Snooze_ maps\n[REST/HTTP](https://en.wikipedia.org/wiki/REST) concepts to Common\nLisp concepts:\n\n| HTTP/REST concept                    | Snooze CL concept                   |\n| :----------------------------------- | ----------------------------------: |\n| REST resource                        | CLOS generic function               |\n| Route                                | CLOS method                         |\n| Verbs (`GET`, `POST`, `DELETE`, etc) | CLOS specializer on first argument  |\n| `Accept:` and `Content-Type:`        | CLOS specializer on second argument |\n| URI path (`/path1/path2/path3)`)     | Required and optional arguments     |\n| URL queries (`?param=value\u0026p2=v2`)   | Keyword arguments                   |\n| Status codes (`404`, `500`, etc)     | CL conditions                       |\n\nThis has many advantages, for example\n\n* since every route is a method, you can `trace` it like a regular\n  function, find its definition with `M-.` or even use `:around`\n  qualifiers;\n* using a regular lambda-list guarantees that URI errors can be\n  spotted early by your compiler;\n* there is no need to write code to \"extract\" arguments from the\n  URI. \n* Since _Snooze_ knows the lambda-list of a route, it can use it\n  to do the reverse of URI matching: generate URIs that perfectly\n  match that same route.\n\n\nTutorial\n--------\n\nConsider the code sample presented [above](#snooze). Let's pick up\nwhere we left off, and build a bit more of `lispdoc`, our\ndocstring-manipulating application. We'll see how to:\n\n* [understand _Snooze_'s REST resources](#resources-as-generic-functions)\n* [dispatch on HTTP methods and content-types](#content-types)\n* [generate and encode compatible URIs](#uri-generation)\n* [grafully handle failure conditions](#controlling-errors)\n* [control conversion of URI arguments](#how-snooze-converts-uri-components-to-arguments)\n* [refactor routes without changing the API](#tighter-routes)\n* [hook _Snooze_ into the backend of your choice](#other-backends)\n\nThis tutorial assumes you're using a recent version of\n[quicklisp][quicklisp] so start by entering this into your REPL.\n\n```lisp\n(push \"path/to/snoozes/parent/dir\" quicklisp:*local-project-directories*)\n(ql:quickload :snooze)\n```\n\nMake sure you keep an eye on the docstrings of the functions\nmentioned, *they are where the real API reference lives*. Find them\nall, appropriately, in the\n[api.lisp](https://github.com/joaotavora/snooze/blob/master/api.lisp)\nfile.\n\n### Resources as generic functions\n\nAn important detail that was elided from the initial sample is that,\nin _Snooze_:\n\n* a REST _resource_ is implemented a CLOS generic\nfunction.\n* The _operations_ (`GET`, `POST`, `DELETE`, etc...) accepted by a\nresource are implemented as CLOS methods on that generic function\n\nWhen a HTTP request is received, _Snooze_ arranges for its URI to be\ntranslated into a generic function name and its remaining properties\n(verb, content-type, additional URI bits) to be translated into\narguments for that function. _Snooze_ then calls that function with\nthose arguments and CLOS does the rest:\n\n* if one of the methods of this generic function matches, its body is\ncalled and the HTTP client sees a nice response;\n\n* otherwise a condition is signalled and _Snooze_ takes care that the\nHTTP client sees the correct error code.\n\n\n\nUnder the hood, `defroute` is actually a really thin wrapper on\n`defmethod`. You can even use `defmethod` directly if you prefer:\n\n```lisp\n(defmethod lispdoc\n            ((snooze-verbs:http-verb snooze-verbs:post)\n             (snooze-types:type snooze-types:text/plain) name\n             \u0026key (package :cl) (doctype 'function))\n   (setf (documentation (find-symbol-or-lose name package) doctype)\n         (payload-as-string)))\n```\n\nLikewise there is a `defresource` form that is equivalent to\n`defgeneric`. It may be left out since it is implicit in the first\n`defroute` call.\n\nThis means we could have defined the above application in an\nequivalent terser form:\n\n```lisp\n(defresource lispdoc (verb content-type name \u0026key)\n  (:route (:get :text/* name \u0026key (package :cl) (doctype 'function))\n    (or (documentation (find-symbol-or-lose name package) doctype)\n        (http-condition 404 \"Sorry, ~a doesn't have any ~a doc\" name doctype)))\n  (:route (:post :text/plain name \u0026key (package :cl) (doctype 'function))\n    (setf (documentation (find-symbol-or-lose name package) doctype)\n          (payload-as-string))))\n```\n\n### Content-Types\n\nLet's start by serving docstrings in HTML. As seen above, we already\nhave a route which serves plain text:\n\n```lisp\n(defroute lispdoc (:get :text/* name \u0026key (package :cl) (type 'function))\n  (or (documentation (find-symbol-or-lose name package) type)\n      (http-condition 404 \"Sorry no ~a doc for ~a\" type name)))\n```\n\nTo add a similar route for the content-type `text/html`, we just\nnotice that `text/html` *is* `text/*`. Also because routes are really\nonly CLOS methods, the easiest way is:\n\n```lisp\n(defroute lispdoc :around (:get :text/html name \u0026key \u0026allow-other-keys)\n  (format nil \"\u003ch1\u003eDocstring for ~a\u003c/h1\u003e\u003cp\u003e~a\u003c/p\u003e\"\n          name (call-next-method)))\n```\n\nThis will do fine for now. Of course, later we should probably escape\nthe HTML with something like [cl-who][cl-who]'s\n`escape-string-all`. We might also consider removing the `:around`\nqualifier and use a helper function shared by two routes.\n\nLet's try our hand at implementing an important part of the API:\n`POST` requests with JSON content:\n\n```lisp\n(defroute lispdoc (:post \"application/json\" name \u0026key (package :cl) (doctype 'function))\n  (let* ((json (handler-case\n                   ;; you'll need to quickload :cl-json\n                   (json:decode-json-from-string\n                    (payload-as-string))\n                 (error (e)\n                   (http-condition 400 \"Malformed JSON (~a)!\" e))))\n         (sym (find-symbol-or-lose name package))\n         (docstring (cdr (assoc :docstring json))))\n    (if (and sym docstring doctype)\n        (setf (documentation sym doctype) docstring)\n        (http-condition 400 \"JSON missing some properties\"))))\n```\n\n### URI generation\n\nOur application has a growing number of routes that work fine,\nprovided the use knows how to type them.  Because this is increasingly\ncomplicated as more and more routes are added, it is very often the\ncase that we'll want parts of the our REST application to generate\nURI's that match its own routes. Probably, the most common case is\nproviding a link to a specific resource in an HTML response.\n\nThis is very easy to do in _Snooze_, as it can automatically generate\nthe URIs for a resource. You first need to get a \"genpath\" function\nfor your resource. Just do:\n\n\n```lisp\n(defgenpath lispdoc lispdoc-path)\n```\n\nOr, alternatively, pass `:genpath` to `defresource`.\n\n```\n(defresource lispdoc (verb ct symbol) (:genpath lispdoc-path))\n```\n\nThe newly generated `lispdoc-path` has an argument list that perfectly\nmatches your route's arguments:\n\n```lisp\n(lispdoc-path 'defroute :package 'snooze)\n  ;; =\u003e \"/lispdoc/defroute?package=snooze\"\n(lispdoc-path 'defun)\n  ;; =\u003e \"/lispdoc/defun\"\n(lispdoc-path '*standard-output* :doctype 'variable)\n  ;; =\u003e \"/lispdoc/%2Astandard-output%2A?doctype=variable\"\n(lispdoc-path '*standard-output* :FOO 'hey)\n  ;; error! unknown \u0026KEY argument: :FOO\n```\n\nNotice the automatic URI-encoding of the `*` character and how the\nfunction errors on invalid keyword arguments that would produce an\ninvalid route.\n\nPath generators are useful, for example, when write HTML links to your\nresources. In our example, let's use it to guide the user to the\ncorrect URL when an easily-fixed 404 happens:\n\n```lisp\n(defun doc-not-found-message (name package doctype)\n  (let* ((othertype (if (eq doctype 'function) 'variable 'function))\n         (otherdoc (documentation (find-symbol-or-lose name package) othertype)))\n    (with-output-to-string (s)\n      (format s \"There is no ~a doc for ~a.\" doctype name)\n      (when otherdoc\n        (format s \"\u003cp\u003eBut try \u003ca href=~a\u003ehere\u003c/a\u003e\u003c/p\u003e\"\n                (lispdoc-path name :package package :doctype othertype))))))\n\n(defroute lispdoc (:get :text/html name \u0026key (package :cl) (doctype 'function))\n  (or (documentation (find-symbol-or-lose name package) doctype)\n      (http-condition 404 (doc-not-found-message name package doctype))))\n```\n\nIf you now point your browser to:\n\n```\nhttp://localhost:9003/lispdoc/%2Astandard-output%2A?doctype=variable\n```\n\nYou should see a nicer 404 error message. **Except you don't (!)**,\nbecause, by default, _Snooze_ is very terse with error messages and we\nhaven't told it not to be. So don't worry, the next sections explains\nhow to change that.\n\nControlling errors\n------------------\n\nErrors and unexpected situations are part of normal HTTP life. Many\nwebsites and REST services not only return an HTTP status code, but\nalso serve information about the conditions that lead to an error, be\nit in a pretty HTML error page or a JSON object describing the\nproblem.\n\n_Snooze_ tries to make it possible to precisely control what\ninformation gets sent to the client. It uses a generic function and\ntwo variables:\n\n* `explain-condition (condition resource content-type)`\n* `*catch-errors*`\n* `*catch-http-conditions*`\n\nOut of the box, there are no methods on `explain-condition` and the\ntwo variables are set to `t`.\n\nThis means that any HTTP condition or a Lisp error in your application\nwill generate a very terse reply in plain-text containing only the\nstatus code and the standard reason phrase.\n\nYou can amend this selectively by adding`explain-condition`\nmethods that explain HTTP conditions politely in, say, HTML:\n\n```lisp\n(defmethod explain-condition ((condition http-condition)\n                              (resource (eql #'lispdoc))\n                              (ct snooze-types:text/html))\n               (with-output-to-string (s)\n                 (format s \"\u003ch1\u003eTerribly sorry\u003c/h1\u003e\u003cp\u003eYou might have made a mistake, I'm afraid\u003c/p\u003e\")\n                 (format s \"\u003cp\u003e~a\u003c/p\u003e\" condition)))\n```\n\nThe above explains only HTTP conditions that are the client's fault, but you can use the same technique to explain *any* error, like so:\n\n```lisp\n(defmethod explain-condition ((error error) (resource (eql #'lispdoc)) (ct snooze-types:text/html))\n               (with-output-to-string (s)\n                 (format s \"\u003ch1\u003eOh dear\u003c/h1\u003e\u003cp\u003eIt seems I've messed up somehow\u003c/p\u003e\")))\n```\n\nFinally, you can play around with `*catch-errors*` and\n`*catch-http-conditions` (see their docstrings). I normally leave\n`*catch-http-conditions*` set to `t` and `*catch-errors*` set to\neither `:verbose` or `nil` depending on whether I want to do debugging\nin the browser or in Emacs.\n\nHow Snooze converts URI components to arguments\n-----------------------------------------------\n\nYou might have noticed already that the arguments passed to the CLOS\ngeneric functions that represent resources are actual Lisp symbols\nextracted from the URI, whereas other frameworks normally pass them as\nstrings.\n\nWhat are the advantages of this? Let's drift from the `lispdoc`\nexample a bit. Consider this fragment of a Beatle-listing app.\n\n\n```lisp\n(defclass beatle () ((id      :initarg :id)\n                     (name    :initarg :name    :accessor name)\n                     (guitars :initarg :guitars :accessor number-of-guitars)))\n\n(defparameter *beatles*\n           (list (make-instance 'beatle :id 1 :name \"John\" :guitars 1)\n                 (make-instance 'beatle :id 2 :name \"Paul\" :guitars 2)\n                 (make-instance 'beatle :id 3 :name \"Ringo\" :guitars 0)\n                 (make-instance 'beatle :id 4 :name \"George\" :guitars 10)))\n\n(defroute beatles (:get \"text/plain\" \u0026key (key 'number-of-guitars) (predicate '\u003e))\n  (assert-safe-functions key predicate)\n  (format nil \"~{~a~^~%~}\"\n          (mapcar #'name\n                  (sort (copy-list *beatles*) predicate :key key))))\n\n(defgenpath beatles beatles-path)\n```\n\nThe `defgenpath` form makes `beatles-path` be a function of two\nkeyword arguments, `:key` and `:predicate` that returns the perfect\nURI for accessing the `beatles` route. Among other things you can name\nregular functions (like `\u003c` and `string-lessp` in this example) by\ntheir symbols, as you would in pure Lisp.\n\n```lisp\nCL-USER\u003e (beatles-path :key 'number-of-guitars :predicate '\u003c)\n\"/beatles/?key=number-of-guitars\u0026predicate=%3C\"\nCL-USER\u003e (beatles-path :key 'name :predicate 'string-lessp)\n\"/beatles/?key=name\u0026predicate=string-lessp\"\n```\n\nSure enough, feeding these URIs to the HTTP client causes the function\n`beatles` to be called with exactly the same symbols that you passed\nto `beatles-path`.\n\nNow, if you're thinking that this doesn't fit needs, know that it is\nmerely a default behaviour, and entirely configurable: if you really\nwant to have the URI path `foo/bar/baz` become the strings `\"foo\"`,\n`\"bar\"` and `\"baz\"` in your application you merely need to add a CLOS\nmethod to the each of the generic functions `read-for-resource` and\n`write-for-resource`.\n\nNevertheless, I recommend you keep the default:\n\n* The default `read-for-resource` uses a very locked down version of\n   `cl:read-to-string` that doesn't intern symbols (for security),\n   allow any kind of reader macros or read anything more complicated\n   than a number, a string or a symbol.\n\n* The default `write-for-resource` does the inverse: it writes onto a\n  string of any object so that `read-for-resource` can reconstruct\n  that object from the string (so long as the object is a secure thing\n  to serialize over URI).\n\nThere is perhaps a better way to influence the mapping between URIs\nand arguments. To that effect, two other functions are discussed in\nthe next section: `arguments-to-uri` and `uri-to-arguments`.\n\nTighter routes\n--------------\n\nLet's recall the `lispdoc` app. The routes we have until now are\nfunctions of a string. To convert them into actual symbols they need:\n\n* the `find-symbol-or-lose` helper;\n* an additional `:package` keyword arg.\n\nThis isn't pretty: it would be nicer if routes were functions of a\nsymbol. After all, in Common Lisp, passing symbols around shouldn't\nforce you to pass their packages separately!\n\nSo basically, we want to write our methods like this:\n\n```lisp\n(defroute lispdoc (:get :text/* (sym symbol) \u0026key (doctype 'function))\n  (or (documentation sym doctype)\n      (http-condition 404 (doc-not-found-message sym doctype))))\n```\n\nActually, this will work *just fine* out of the box. Oh wait, now our routes\nlook like this:\n\n```lisp\n(lispdoc-path 'cl-ppcre:scan)\n  ;; =\u003e \"/lispdoc/cl-ppcre%3Ascan\"\n(lispdoc-path 'ql:quickload)\n  ;; =\u003e \"/lispdoc/quicklisp-client%3Aquickload\"\n```\n\nCompare these to the routes at the very top of this document:\n\n```lisp\n  /lispdoc/scan?package=cl-ppcre\n  /lispdoc/quickload?package=ql\n```\n\nYou might be dissapointed that the new ones are not as human-readable\n(the `%3A` encoding for the `:` looks would look slightly bizarre to a\nuser reading it in the browser's address bar). But even if you don't\ncare about appearance and find them perfectly acceptable, it is\nconceivable that we had already published the routes of the older REST\nAPI to the world.\n\nSo, to keep that API, we need to change the implementation without\nchanging the interface. This is where `uri-to-arguments` and its\nreciprocal `arguments-to-uri` come in handy.  These generic functions\nhave default implementations for all resources that can be leveraged\nfor surgical tweaks like the one we need here:\n\n* `uri-to-arguments` receives an URI string, and make it compute a\n  values-list of \"plain\" and keyword arguments that are passed to the\n  route (after the verb and content type). In our case, we first parse\n  the plain symbol name and `:package` using `call-next-method`, then\n  compute an actual symbol. We also make sure to not pass `:package`\n  to our new route, as it doesn't accept it.\n\n* `arguments-to-uri` is the function that allows the `genpath`\n  function to produce matching URIs. It does the reverse, producing an\n  URI string.  In this case it takes a symbol in `plain-args`, After\n  extracting its package and massaging it into an uninterned symbol,\n  we also use `call-next-method` to simplify things.\n\n```lisp\n(defmethod uri-to-arguments ((resource (eql #'lispdoc)) uri)\n  (multiple-value-bind (plain-args keyword-args)\n      (call-next-method)\n    (let* ((sym-name (string (first plain-args)))\n           (package-name (or (cdr (assoc :package keyword-args)) 'cl))\n           (sym (find-symbol sym-name package-name)))\n      (unless sym\n        (http-condition 404 \"Sorry, no such symbol\"))\n      (values (cons sym (cdr plain-args))\n              (remove :package keyword-args :key #'car)))))\n\n(defmethod arguments-to-uri ((resource (eql #'lispdoc)) plain-args keyword-args)\n  (let ((sym (first plain-args)))\n    (call-next-method resource\n                      (list sym)\n                      (cons `(:package . ,(make-symbol\n                                           (package-name (symbol-package sym))))\n                            keyword-args))))\n```\n\nWe can now safely rewrite the remaining routes in much simpler\nfashion. Here's the rest of the application now (notice also how\n`doc-not-found-message` was also simplified)\n\n```lisp\n(defun doc-not-found-message (symbol doctype)\n  (let* ((othertype (if (eq doctype 'function) 'variable 'function))\n         (otherdoc (documentation symbol othertype)))\n    (with-output-to-string (s)\n      (format s \"There is no ~a doc for ~a.\" doctype symbol)\n      (when otherdoc\n        (format s \"\u003cp\u003eBut try \u003ca href=~a\u003ehere\u003c/a\u003e\u003c/p\u003e\"\n                (lispdoc-path symbol :doctype othertype))))))\n\n(defroute lispdoc (:get :text/* (sym symbol) \u0026key (doctype 'function))\n  (or (documentation sym doctype)\n      (http-condition 404 \"No doc found for ~a\" sym)))\n\n(defroute lispdoc (:post :text/plain (sym symbol) \u0026key (doctype 'function))\n  (setf (documentation sym doctype)\n        (payload-as-string)))\n\n(defroute lispdoc (:get :text/html (sym symbol) \u0026key (doctype 'function))\n  (or (documentation sym doctype)\n      (http-condition 404 (doc-not-found-message sym doctype))))\n\n(defroute lispdoc (:post :application/json (sym symbol) \u0026key (doctype 'function))\n  (let* ((json (handler-case\n                   (json:decode-json-from-string\n                    (payload-as-string))\n                 (error (e)\n                   (http-condition 400 \"Malformed JSON! (~a)\" e))))\n         (docstring (cdr (assoc :docstring json))))\n    (setf (documentation sym doctype) docstring)))\n```\n\nOther backends\n--------------\n\n_Snooze_ is web-server agnostic: it's just an URL router.  It does\ncome with two utility functions, `make-clack-app` and\n`make-hunchentoot-app` that will plug into two popular web servers and\nquickly let you jump into the action:\n\n```lisp\n;;; Use hunchentoot directly\n(push (snooze:make-hunchentoot-app) hunchentoot:*dispatch-table*)\n(hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 9003))\n\n;;; Use clack\n(clack:clackup (snooze:make-clack-app) :port 9003)\n```\n\nBut Snooze doesn't \"require\" Clack or Hunchentoot in any sense. So if\nyou want to use any other backend, I suggest you take a look at the\nimplementations of `make-hunchentoot-app` and `make-clack-app`\nfunctions, particularly their use of `snooze:handle-request`.\n\nSupport\n-------\n\nTo ask questions, report bugs, or just discuss matters open an\n[issue][issues] or send me email.\n\n[quicklisp]: http://quicklisp.org\n[asdf]: http://common-lisp.net/project/asdf/\n[hunchentoot]: https://github.com/edicl/hunchentoot\n[sharp-lisp]: irc://irc.freenode.net/#lisp\n[issues]: https://github.com/joaotavora/snooze/issues\n[caveman]: https://github.com/fukamachi/caveman#routing\n[clack]: https://github.com/fukamachi/clack\n[cl-rest-server]: https://github.com/mmontone/cl-rest-server\n[restas]: http://restas.lisper.ru/en/manual/routes.html#routes\n[ningle]: http://8arrow.org/ningle/\n[cl-who]: http://weitz.de/cl-who/#escape-string-all\n[rest]: https://en.wikipedia.org/wiki/Representational_state_transfer\n[restful-routes]: https://en.wikipedia.org/wiki/Representational_state_transfer#Relationship%20between%20URL%20and%HTTP%20methods\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoaotavora%2Fsnooze","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoaotavora%2Fsnooze","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoaotavora%2Fsnooze/lists"}