aboutsummaryrefslogtreecommitdiff

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 specializing instantiate-instance on a subclass of ENDPOINT 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-09-06 Fri 23:44

Validate