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
|
(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"
: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
(setf (lzb:response-cookie "testappsession") "coolsessionbro")
"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."
(jonathan:to-json person))
|