aboutsummaryrefslogtreecommitdiff
path: root/example/lazybones-example.lisp
blob: 5eb00d950bd817b9e5f23d4a372dc22f9f19ec78 (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
(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
  (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
    }

")