-
Notifications
You must be signed in to change notification settings - Fork 19
/
route.lisp
212 lines (192 loc) · 9.38 KB
/
route.lisp
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
(in-package :wookie)
(define-condition use-next-route () ()
(:documentation
"Signals to the routing system to load the next route after the one loaded.
can be used to set up route load chains based on criteria not sent directly
to find-route."))
(define-condition route-error (wookie-error)
((resource :initarg :resource :reader route-error-resource :initform nil))
(:report (lambda (c s) (format s "Routing error: ~s" (route-error-resource c))))
(:documentation "Describes a general routing error."))
(define-condition route-not-found (route-error) ()
(:report (lambda (c s) (format s "Routing error: route not found for ~s" (route-error-resource c))))
(:documentation "Describes a route not found error."))
(defvar *default-vhost* nil
"Defines the default virtualhost that routes use (unless explicitely stated
otherwise). Nil means no vhost (respond to all requests).")
(defun clear-routes ()
"Clear out all routes."
(vom:debug1 "(route) Clearing routes")
(setf (wookie-state-routes *state*) (make-array 0 :adjustable t :fill-pointer t))
(routes-modified))
(defun make-route (method resource fn &key regex case-sensitive allow-chunking buffer-body suppress-100 vhost priority)
"Simple wrapper to make a route object from a set of args."
(let ((scanner (if regex
(cl-ppcre:create-scanner
(concatenate 'string "^" resource "$")
:case-insensitive-mode (not case-sensitive))
resource)))
(list :method method
:resource scanner
:fn fn
:regex regex
:allow-chunking allow-chunking
:buffer-body buffer-body
:suppress-100 suppress-100
:resource-str resource
:vhost vhost
:priority (or priority 0))))
(defun next-route ()
"Lets the routing system know to re-route the current request, excluding this
route from the available options."
(signal 'use-next-route))
(defun find-route (method resource &key exclude host)
"Given a method and a resource, find the best matching route."
(loop for route across (ordered-routes) do
;; don't load excluded routes
(unless (find-if (lambda (ex)
(eq (getf ex :fn) (getf route :fn)))
exclude)
(when (and (let ((route-method (getf route :method)))
;; test for a list of methods as well as exact method match.
;; also allow wildcard method match via :*
(if (listp route-method)
(find method route-method)
(or (eq method route-method)
(eq :* route-method))))
(or (not (getf route :vhost))
;; either exact match the host or match without portnum
(or (equal (getf route :vhost) host)
(when (stringp host)
(equal (getf route :vhost)
(subseq host 0 (position #\: host)))))))
(multiple-value-bind (matchedp matches)
(if (getf route :regex)
(cl-ppcre:scan-to-strings (getf route :resource) resource)
(string= (getf route :resource) resource))
(when matchedp
(let* ((fn (getf route :fn))
(curried-fn (lambda (request response)
(apply fn (append (list request response)
(coerce matches 'list))))))
(setf (getf route :curried-route) curried-fn)
(return-from find-route route))))))))
(defun routes-modified ()
"Reset ordered route cache after routing changes"
(setf (wookie-state-ordered-routes *state*) nil))
(defun ordered-routes ()
"Return the array of routes ordered by their priority,
routes with higher priority being first."
(or (wookie-state-ordered-routes *state*)
(setf (wookie-state-ordered-routes *state*)
(stable-sort (copy-seq (wookie-state-routes *state*))
#'> :key #'(lambda (route)
(getf route :priority))))))
(defun add-route (new-route)
"Add a new route to the table."
(vector-push-extend new-route (wookie-state-routes *state*))
(routes-modified)
(length (wookie-state-routes *state*)))
(defun method-equal (method1 method2)
"Test two route methods (kewords or lists of keywords) for equality."
(if (eq (type-of method1) (type-of method2))
(etypecase method1
(keyword (eq method1 method2))
(list (equal (sort (copy-list method1) #'string<)
(sort (copy-list method2) #'string<))))
nil))
(defun route-equal (route method resource-str)
"Test the property values of :method and :resource-str in a route
plist for equality against a supplied method and resource-str."
(and (method-equal (getf route :method) method)
(string= (getf route :resource-str) resource-str)))
(defun upsert-route (new-route)
"Add a new route to the table. If a route already exists with the same method
and resource string, it is replaced with the new one in the same position the
old route existed in (as to preserve routing order)."
(let ((route-found nil)
(resource-str (getf new-route :resource-str) )
(method (getf new-route :method)))
(unless (zerop (length (wookie-state-routes *state*)))
(loop for i from 0
for route across (wookie-state-routes *state*) do
(when (route-equal route method resource-str)
(setf (aref (wookie-state-routes *state*) i) new-route
route-found t)
(return))))
(unless route-found
(vector-push-extend new-route (wookie-state-routes *state*)))
(routes-modified)
(length (wookie-state-routes *state*))))
(defun clear-route (method resource-str)
"Clear out a route in the routing table."
(vom:debug1 "(route) Clear route ~s" resource-str)
(let* ((new-routes (delete-if
(lambda (route)
(route-equal route method resource-str))
(wookie-state-routes *state*)))
(new-routes (make-array (length new-routes) :initial-contents new-routes :fill-pointer t :adjustable t)))
(setf (wookie-state-routes *state*) new-routes)
(routes-modified)
(values)))
(defmacro defroute ((method resource &key (regex t) (case-sensitive t)
chunk (buffer-body t) suppress-100
(replace t)
(vhost '*default-vhost*)
(priority 0))
(bind-request bind-response &optional bind-args)
&body body)
"Defines a wookie route and pushes it into the route list.
:regex specifies whether resource is a regex or not
:chunk specifies if the route can handle chunked content
:buffer-body tells Wookie to save any body parts that come through before
with-chunking is called
:suppress-100 tells Wookie that we want to send our own `100 Continue` HTTP
response if we get an `Expect: 100-continue` header in the request
:replace tells the routing system to upsert this resource/method set
(instead of just blindly adding it to the end of the list like default)
:priority specifies a the route priority (a number, defaults to 0). Routes
with higher priority values are processed first. Both negative and
positive priorities are acceptable.
bind-request/bind-response are the variable names that the request/response
values are bound to, and bind-args specifies that variable that regex group
matches get sent to (a list)."
(let* ((new-route (gensym))
(ignore-bind-args nil)
(bind-args (if bind-args
bind-args
(progn
(setf ignore-bind-args t)
(gensym))))
;; allow method to be a list of keywords
(method (if (listp method)
`(list ,@method)
method))
(docstring (when (stringp (car body)) (car body)))
(body (if docstring
(cdr body)
body)))
`(let ((,new-route (make-route ,method ,resource
(lambda (,bind-request ,bind-response &rest ,bind-args)
,(or docstring "")
(declare (ignorable ,bind-request))
,@(when ignore-bind-args
`((declare (ignore ,bind-args))))
,@body)
:regex ,regex
:case-sensitive ,case-sensitive
:allow-chunking ,chunk
:buffer-body ,buffer-body
:suppress-100 ,suppress-100
:vhost ,vhost
:priority ,priority)))
(if ,replace
(upsert-route ,new-route)
(add-route ,new-route)))))
(defmacro with-vhost (host &body body)
"Simple wrapper that makes all defroutes in the body bind to a specific vhost:
(with-vhost \"omg.com\"
(defroute ...)
(defroute ...))"
`(let ((*default-vhost* ,host))
,@body))