aboutsummaryrefslogtreecommitdiff
path: root/README.md
blob: 2279400fe6d40d317a60fa7f16f793ce69a436c0 (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
# What

`lazybones` is a small HTTP routing framework.

Features include: 

- Different server backends. (At the moment only Hunchnetoot is supported)
- Modular and "installable" applications.
- Handy macros for provisioning apps and defining endpoints.
- Livecoding 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.


## Example

The following is quick example showing a few 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)

;; 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
  404 'custom-404             ; custom content for error response codes. 
  403 'custom-403)

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

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

(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")))

;; 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'."
  (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" ()
  "Echo the search parameters in a nice list."
  (http-ok (format nil "Query Was:~%~{~a is ~a~%~}~%"
                   (loop for (x . y) in (lzb:request-parameters)
                         collect x
                         collect y))))

(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)))

```

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