* WEEKEND: another HTTP endpoint building library With ~WEEKEND~, endpoints are classes defined with the metaclass ~endpoint~. The ~endpoint~ metaclass provides a number of class-level slots which specify the HTTP method and the route that the class is meant to handle. Compiling the endpoint class automatically builds and registers a hunchentoot handler. Recompiling the class removes the old handler and builds a new one. Instance slots of an ~endpoint~ class hold data necessary to process the request. If a slot is defined with an ~:initarg~, then that slot's value is meant to be supplied by the HTTP request, either from embedded "route variables", query parameters, or fields in a structured request body. Any slot defined without an ~:initarg~ is meant to have its value supplied by some stage in the handler protocol. The handler protocol is used to control the handling of requests. All endpoint classes are required to specialize the ~weekend:handle~ method. Optionally they may also specialize the ~weekend:authenticate~ and ~weekend:authorize~ methods. Before ~handle~ is called, ~authorize~ is called, and before ~authorize~ is called, ~authenticate~ is called. If either ~authenticate~ or ~authorize~ return ~NIL~, then the appropriate HTTP error is returned to the client. Otherwise ~handle~ is assumed to have all it needs to produce the content to be returned to the requesting client. *** Install Get dependencies not in quicklisp: #+begin_src shell cd ~/quicklisp/local-projects/ git clone https://cicadas.surf/cgit/colin/weekend.git git clone https://cicadas.surf/cgit/colin/flatbind.git git clone https://cicadas.surf/cgit/colin/argot.git #+end_src Ensure quicklisp knows about them, then quickload. #+begin_src lisp (ql:register-local-projects) (ql:quickload :weekend) #+end_src ** Examples See [[https://cicadas.surf/cgit/colin/weekend.git/tree/examples][examples]] for a few examples. ** Advantages of the Approach OOPyness is an advantage. Object orientation allows one to: - Recycle logic by defining protocol methods on mixin superclasses of endponit classes. Whole regions of a site or API can easly be extended or modified. - ~ENDPOINT~ itself can be sublcassed for special endpoint definition logic. E.g. ensuring a route prefix without having to type it each time can be acheived by specializing ~instantiate-instance~ on a subclass of ~ENDPOINT~. - You can use the reified endpoints to automatically generate nice documentation. (Less than nice documentation is already provided via the function ~weekend:print-all-route-documentation~.) - Similarly, you can automatically generate API client code for your langauge of choice. ** Defining Routes All routes are defined by supplying route parts. Each route part is a string and these strings are combined into a regular expression. Match groups inside the regular expression are extracted and parsed according to the value of the ~:extractors~ slot. If there are N match groups in a route, then there must be N extractors. E.g #+begin_src lisp (defclass example () ((foo-id :type integer :reader foo-id :initarg :foo-id) (bar-val :type string :reader bar-val :initarg :bar-val)) (:metaclass weekend:endpoint) (:method . :get) (:route-parts "foo" "([0-9]+)" "bar" "(blah|nah)") (:extractors (:foo-id parse-integer) :bar-val)) #+end_src In ~:route-parts~ there are four parts, two match groups. The ~:extractors~ slot has two extractor specs. The parts will be combined into a regular expression: ="^/foo/([0-9]+)/bar/(blah|nah)$"= The first match group matches a string of digits. That string is parsed by ~parse-integer~ and then supplied to the ~:foo-id~ initarg. The second match group matches either ~"blah"~ or ~"nah"~ and is passed as-is tot he ~:bar-val~ initarg. This class would handle the route =GET /foo/322/bar/nah= for example. You might be asking yourself "why not just write the regex as a string?" Well thats a good question. You can actually do that (dig into the ~endpoint~ metaclass yourself to see how), but the reason I prefer ~:route-parts~ is to allow for defining your regex pattern groups outside of the route as module parameters. See examples/dice-roller.lisp. ** Passing Values in Query Params Here we modify the above example by only passing one argument in the route, leaving the other to be a query parameter. #+begin_src lisp (defclass example2 () ((foo-id :type integer :reader foo-id :initarg :foo-id) (bar-val :type string :reader bar-val :initarg :bar-val :initform (weekend:slot-required 'bar-val 'example2)) (:metaclass weekend:endpoint) (:method . :get) (:route-parts "foo" "([0-9]+)" "bar") (:extractors (:foo-id parse-integer))) #+end_src This would handle routes like =GET /foo/323/bar?bar-val=moo= If ~bar-val~ had not been supplied in the query parameters, the ~slot-required~ error would be signalled and the client would receive a 400 HTTP response code.