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
|
(asdf:load-system "lazybones-hunchentoot")
(defpackage #:lazybones-example
(:use #:cl)
(:local-nicknames (#:lzb #:lazybones))
(:import-from #:lazybones
#:defendpoint*
#:http-err))
(in-package :lazybones-example)
;; 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 :o~%" (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"
:prefix "/eg"
:description "Just an API that defines some endpoints. These
endpoints aren't meant to accomplish anything. Merely testing 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.
;; The general syntax is: DEFENDPOINT* HTTP-METHOD ROUTE-TEMPLATE QUERY-PARAMETERS OPTIONS DOCSTRING BODY ...
(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
(lzb:set-response-cookie "testappsession" "coolsessionbro" :path "/" :domain "localhost")
"true")
;; The next route defines a route variable WHO
(defendpoint* :get "/hello/:who:" () ()
"Echos back Hello WHO"
(format nil "Hello ~a~%" who)) ; use the route variable here
(defendpoint* :post "/hello/:who:" ()
(:auth t) ; use the app's default authorizor
"Echo's back 'Hello WHO, I got your message BODY' where BODY is the post body."
(print (lzb:request-header :content-type))
(let ((body (lzb:request-body)))
(format nil "Hello ~a, I got your message ~a~%"
who body)))
;; Some helpers, these are used to parse url variables and query
;; parameters. Their docstrings are used in the API documentation
(defun int (string)
"An Integer"
(parse-integer string))
(defun str (string)
"A String"
string)
;; In the following, two query parameters are specififed. NAME is
;; meant to be a string and AGE is an integer. If AGE is not an integer,
;; a 500 error will be returned. The syntax is (VAR PARSER)
(defendpoint* :get "/search" ((name str) (age int)) ()
"Echo the search parameters in a nice list."
(format nil "Name: ~a~%age: ~a~%" name age))
(defun crapshoot-authorizer ()
"Randomly decides that the request is authorized"
(< 5 (random 10)))
(defendpoint* :post "/crapshoot" ()
(:auth 'crapshoot-authorizer) ; use a custom authorizer
"Echos back 'You made it' if the request was authorized"
"You made it")
;; Route variables can accept parsers / preformatters
;; these will parse a value and supply it to the argument of the handler.
(defendpoint* :get "/random/:lo int:/:hi int:" () ()
"Echo back a random number between lo and hi"
(if (< lo hi)
(format nil "The number is: ~a~%"
(+ lo (random (- hi lo))))
(http-err 404))) ; Can't find a number X such that LO >= HI and LO < X < HI
(defun person-by-id (id)
"ID of a person"
;; 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") ; override the app's default content type for HTTP responses
"Returns a json representation of the [Person](#person)."
(jonathan:to-json person))
(lzb::set-definition
"Person" "#person"
"An instance of person. As JSON, it looks like:
{
\"NAME\" : string ,
\"OCCUPATION\" : string ,
\"ID\" : integer
}
")
(defclass animal ()
((genus :initarg :genus :documentation "The genus")
(species :initarg :species :documentation "The species")
(population :initarg :population :documentation "Population on Earth")
(habitat :initarg :habitat :documentation "Where the animal lives"))
(:documentation "An animal"))
;; add a documentation definition for animal class, the provided slots
;; will show up in the api docs
(lzb:add-class-to-definitions (lzb:app)
'animal
'genus 'species 'population 'habitat)
(defvar +dummy-animal+
(make-instance 'animal
:genus "Pseudocheirus"
:species "Peregrinis"
:population "Unknown"
:habitat "Australia"))
;; you can refer to definitions in docstrings
(defendpoint* :get "/animal/:genus:/:species:" () ()
"Prints information about [Animal](#animal) specified by GENUS and SPECIES"
(if (and (string-equal genus "Pseudocheirus")
(string-equal species "Peregrinis"))
(with-output-to-string (out)
(with-slots (genus species population habitat) +dummy-animal+
(format out "The ~a ~a lives in ~a. Population: ~a~%"
genus species population habitat)))
(http-err 404)))
|