aboutsummaryrefslogtreecommitdiff
path: root/README.org
blob: e00eb8c406eb847458034815badfd06eee5df5ea (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
* 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.

- 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~.


** 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 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.

** 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.