aboutsummaryrefslogtreecommitdiff
path: root/README.md
blob: 5d850709d21b83888321aa81c2368e0b2c8ca95f (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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# What

`lazybones` is a small HTTP routing framework.

Features include: 

- Different server backends. (At the moment only Hunchentoot is supported)
- Modular and "installable" applications.
- Handy macros for provisioning apps and defining endpoints.
- Livecoding supported for your endpoint handlers and application configurations.
- Automatic documentation generation for `lazybones:app` instances.

Although lazybones can be used to develop and serve page-oriented web
sites, it has been written to help me develop "self documenting" HTTP
APIs.


## Main Components 

The main components in `lazybones` are the two classes: `lazybones:app`
and `lazybones:endpoint`.  

Endpoints are objects that represent everything required to handle an
HTTP request.  Endpoints are collected into groups called apps.  

Apps, in addition to being collections of endpoints, are the main unit
of development for larger lazybones projects. Apps can be installed to
running servers on the fly as whole units. If desired, they can also
be uninstalled.  Apps are meant to be devolped in a
one-app-per-package manner.  That is, although you can create and work
with multiple apps in a single Common Lisp package, `lazybones`
encourages you to limit yourself to one per package.  

See the example below for more.

## A Hello World Example 

``` lisp
(asdf:load-system "lazybones-hunchentoot")

(defpackage #:hello-lazybones
  (:use #:cl)
  (:local-nicknames (#:lzb #:lazybones))
  (:import-from #:lazybones #:defendpoint* #:http-ok))

(in-package :hello-lazybones)

(defendpoint* :get "/hello/:name:" () ()
  (http-ok (format nil "Welcome to Lazybones, ~a" name)))

(defvar *my-server* (lzb:create-server))
(lzb:install-app *my-server* (lzb:app))
(lzb:start-server *my-server*)
```

Go ahead and test this sever out with curl:

``` shell

$ curl -v http://localhost:8888/hello/colin
*   Trying 127.0.0.1:8888...
* Connected to localhost (127.0.0.1) port 8888 (#0)
> GET /hello/colin HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.74.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 27
< Date: Wed, 09 Feb 2022 12:26:01 GMT
< Server: Hunchentoot 1.3.0
< Content-Type: text/html; charset=utf-8
< 
* Connection #0 to host localhost left intact
Welcome to Lazybones, colin

```



## A Showcase Example

The following is quick example showing  a few more things that `lazybones` can do.

``` lisp
(asdf:load-system "lazybones-hunchentoot")

(defpackage #:lazybones-test
  (:use #:cl)
  (:local-nicknames (#:lzb #:lazybones))
  (:import-from #:lazybones
                #:defendpoint*
                #:http-ok
                #:http-err))

(in-package :lazybones-test)

;; first make a server and add some custom error responses

(defvar *server* (lzb:create-server :port 8888))

(defun custom-404 ()
  (format nil "~a wasn't found :(" (lzb:request-path))) ; can use request functiosn

(defun custom-403 ()
  "You, in particular, can't do that. :P ")

(defun custom-500 ()
  "Bah. Error.")

(lzb:set-canned-response *server* 404 'custom-404 "text/plain" )
(lzb:set-canned-response *server* 403 'custom-403 "text/plain" )
(lzb:set-canned-response *server* 500 'custom-500 "text/plain")

;; PPROVISION-APP makes an app. You can supply an optional name, a symbol. 
;; In lieu of a supplied name, the name of the package is used as the app's name.
(lzb:provision-app ()
  :title "Lazybones Demo App"
  :version "0.0.0"
  :description "Just an API that defines some endpoints. These
  endpoints aren't meant to accomplish anything. merely to test out
  the lazybones HTTP routing framework."

  :content-type "text/plain"   ; default content type of server responses.
  :auth 'post-authorizer)      ; default authorizor for requests that need it

(defun post-authorizer ()
  "Request is authorized if it contains the right TESTAPPSESSION
  cookie. Obtain such a cookie by posting to the /login endpoint."
  (string-equal "coolsessionbro" (lzb:request-cookie "testappsession")))

;; now we install the app to the server 
(lzb:install-app *server* (lzb:app)) ; (app) is the default app for this package

;; DEFENDPOINT* is a macro to define an endpoint and install it into the 
;; app whose name is the current package anme. DEFENDPOINT (without the *)
;; allows you to explictly specify the app where the endpoint is installed.

(defendpoint* :post "/login" () ()
  "Dummy login endpoint for returning a session cookie. Always returns
  the \"true\" and sends a set-cookie header, setting 'testappsession'
  to 'coolsessionbro'."
  (print (lzb:request-body)) ; dummy implementation, prints post body to stdout
  (setf (lzb:response-cookie "testappsession") "coolsessionbro")
  (http-ok "true"))

(defendpoint* :get "/hello/:who:" () ()
  "Just says hello to WHO"
  (http-ok (format nil "Hello ~a" who)))

(defendpoint* :post "/hello/:who:" ()  (:auth t)  ; use the default authorizor for the app
  "Post something to hello who"
  (print (lzb:request-header :content-type))
  (let ((body (lzb:request-body)))
    (http-ok (format nil "Hello ~a, I got your message ~a"
                     who body))))

(defendpoint* :get "/search" ((name identity) (age to-int)) ()
  "Echo the search parameters in a nice list."
  (http-ok (format nil "Name: ~a~%age: ~a~%" name age)))

(defun crapshoot-authorizer () ()
  "Randomly decides that the request is authorized" 
  (< 5 (random 10)))

(defendpoint* :post "/search" () (:auth 'crapshoot-authorizer)  ; use custom authorizer
  "Echo the search parameters in a nice list, but also has a post-body"
  (http-ok
   (with-output-to-string (out) 
     (format out "Query Was:~%~{~a is ~a~%~}~%"
             (loop for (x . y) in (lzb:request-parameters)
                   collect x
                   collect y))
     (terpri)
     (format out "Decoded Post Body: ~s~%" (lzb:request-body)))))


(defun to-int (string)
  "An Integer"
  (parse-integer string))

;; route variables can accept parsers / preformatters
;; these will parse a value and supply it to the argument of the handler.
;; int eh following CATEGORY is an int

(defendpoint* :get "/search/:category to-int:" () ()
  "Echo the search back, but in a specific category"
  (assert (typep category 'integer)) ; just to show you.
  (http-ok 
    (format nil "Searching in ~a with parameters:~%~{~a = ~a~%~}~%"
                category
                (loop for (x . y) in (lzb:request-parameters)
                      collect x
                      collect y))))

(defun person-by-id (id)
  "A Person Instance"
  ;; The real thing might perform some database operation here. If the
  ;; operation failed, an error could be signalled, in which case a
  ;; 500 response would be sent to the client.
  (list :name "Colin"  :occupation "Macrologist" :id (parse-integer id)))

(defendpoint* :get "/person/:person person-by-id:" () (:content-type "application/json")
  "Returns a json representation of the person."
  (http-ok
   (jonathan:to-json person)))

```

Then use your favorite markdown processor to generate HTML or whatever
your preferred documentation distribution format is. See
`example/lazybones-test-docs.md` for an example of the documentation
generator output.


## Backends 

**WARNING** Users can mostly ignore thissection. The API for alternate backends is in
flux.

To implement a new backend for lazybones, consult the
`lazybones.backend` package. Define a new system and a new package
that `uses` the lazybones.backend package. Implement functions for
each symbol.  Consult the `lazybones-hunchentoot` system for an
example backend.