Table of Contents
1 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.
1.1 Install
Get dependencies not in quicklisp:
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
Ensure quicklisp knows about them, then quickload.
(ql:register-local-projects)
(ql:quickload :weekend)
1.2 Examples
See examples for a few examples.
1.3 Discussion
1.3.1 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.
- You can use the reified endpoints to automatically for your
API. See the function
weekend:print-all-route-documentation
. - Similarly, you can automatically generate API client code for your langauge of choice.
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 specializinginstantiate-instance
on a subclass ofENDPOINT
to alter the:route-parts
.
1.3.2 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
(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))
In :route-parts
there are four parts. Two of those parts are match
groups: ([0-9]+)
and (blah|nah)
. The :extractors
slot has two
extractor specs.
The route 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 to the :bar-val
initarg.
This class would handle the route GET /foo/322/bar/nah
for
example. It would not handle POST /foo/322/bar/nah
nor GET
/foo/xxx/bar/nah
. In both latter caes a 404 would be returned to the
client.
You might be asking yourself "why not just write the regex as a
string?" Well that's 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 that it allows for defining reusable regex
patterns and storing them in global parameters. See
examples/dice-roller.lisp.
1.3.3 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.
(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)))
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.
1.3.4 DEFENDPOINT
Macro
Though not necessary, users may wish to avail themselves of a small
endpoint-defining language called DEFENDPOINT
.
See the defendpoint examples.
DEFENDPOINT
was made using argot, a grammar-driven approach for
defining DSL macros. As such, a docstring describing the grammar is
generated and attached to the DEFENDPOINT
macro - I suggest you
consult it.
(print (documentation 'weekend:defendpoint 'function))
Created: 2024-11-29 Fri 09:20