File: defurlmethod.lisp

package info (click to toggle)
araneida 0.90.1-dfsg-2
  • links: PTS
  • area: main
  • in suites: etch, etch-m68k
  • size: 700 kB
  • ctags: 643
  • sloc: lisp: 4,878; perl: 166; sh: 109; makefile: 34
file content (302 lines) | stat: -rw-r--r-- 14,884 bytes parent folder | download | duplicates (2)
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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
(in-package araneida)

(eval-when (:compile-toplevel :load-toplevel :execute)
  (defvar *url-methods-seen* nil
    "This holds a list of what url methods have been seen - so the defurlmethod macro knows
when/if to declare a companion function. Used at macrotime"))

(defvar *url-methods* nil
  "Holds a list of (method-name type required-parameters key-parameters lambda) for each urlmethod.
Used at runtime to figure out which method to use, and stores the method itself.
Type is either :tainted or :untainted, and determines whether the values are tainted before being
handed over to the urlmethod.")

(defun method-method-name (x)
  (first x))
(defun method-type (x)
  (second x))
(defun method-function-parameters (x)
  (third x))
(defun method-required-parameters (x)
  (fourth x))
(defun method-key-parameters (x)
  (fifth x))
(defun method-all-parameters (x)
  (sixth x))
(defun method-lambda (x)
  (seventh x))

;;;;;;;;;;;; Conditions
(define-condition urlmethod-error (serious-condition)
  ())
(define-condition urlmethod-compile-error (urlmethod-error)
  ())

(define-condition no-urlmethod (urlmethod-error)
  ((method-name :initarg :method-name :reader no-urlmethod-method-name)
   (parameters  :initarg :parameters  :reader no-urlmethod-parameters))
  (:report (lambda (condition stream)
	     (format stream "No urlmethod matched (~A ~@[~A~])"
		     (no-urlmethod-method-name condition)
		     (no-urlmethod-parameters  condition))))
  (:documentation "At compile time we can't tell whether or not a urlmethod call
will have a match. If the call satisfies no method constraints, this is signaled."))

(define-condition too-many-urlmethods-matched (urlmethod-error)
  ((matched-methods :initarg :matched-methods :reader too-many-urlmethods-matched-matched-methods)
   (method-name :initarg :method-name :reader too-many-urlmethods-matched-method-name)
   (parameters  :initarg :parameters  :reader too-many-urlmethods-matched-parameters))
  (:report (lambda (condition stream)
	     (format stream "More than one urlmethod matched the call (~A ~@[~A~]). Methods matched: ~A"
		     (too-many-urlmethods-matched-method-name condition)
		     (too-many-urlmethods-matched-parameters condition)
		     (too-many-urlmethods-matched-matched-methods condition))))
  (:documentation "If precendence does not yield one method from the methods which have satisfied constraints,
this is signaled."))

(define-condition urlmethod-unknown-keyword (urlmethod-compile-error)
  ((unknown :initarg :unknown :reader urlmethod-unknown-keyword-unknown)
   (lambda-list :initarg :lambda-list :reader urlmethod-unknown-keyword-lambda-list))
  (:report (lambda (condition stream)
	     (format stream "Unknown keyword ~A ~@[in lambda list ~A~]"
		     (urlmethod-unknown-keyword-unknown condition)
		     (urlmethod-unknown-keyword-lambda-list condition))))
  (:documentation "Signaled when defurlparameter contains an unknown keyword"))

(define-condition urlmethod-function-parameter-mismatch (urlmethod-compile-error)
  ((other-methods-info :initarg :other-methods-info :reader urlmethod-function-parameter-mismatch-other-methods-info)
   (parameters :initarg :parameters :reader urlmethod-function-parameter-mismatch-parameters))
  (:report (lambda (condition stream)
	     (format stream "~A has a different number of function parameters from previously defined methods: ~A"
		     (urlmethod-function-parameter-mismatch-parameters condition)
		     (urlmethod-function-parameter-mismatch-other-methods-info condition))))
  (:documentation "Function parameters must be constant across all urlmethods of the same name.
parameters refers to the parameters declared in this defurlmethod. other-methods-info refers to the
method-info of previously defined urlmethods of the same name."))

;;;;;;;;;;;; Functions

(defun satisfy-spec-p (spec value)
  "Given a specialization, determine if the value meets that specialization"
  (if (symbolp spec)
      (not (zerop (length value))) ; if it's just a variable, just check to make sure the string isn't blank
      (apply (first (second spec)) value (rest (second spec))))) ; otherwise it's a function call

(defun fulfills-specialization (required-specializations parameters)
  "Given a list of required specializations, determine if the url alist passed (parameters) meets that specialization"
  (if (null required-specializations)
      t
      (let* ((spec (car required-specializations))
	     (param-value (second (assoc (symbol-name (parameter-name-from-url-specialization spec))
					 parameters
					 :test #'string-equal))))
	(if param-value
	    (if (satisfy-spec-p spec param-value)
		(fulfills-specialization (rest required-specializations) parameters)
		nil)
	    nil))))

(defun remove-not-greatest (elements key &key (max #'max) (min #'min))
  "Removes all elements of list that are not equal to the highest return value of key over the list.
Returns the unremoved elements and nil if nothing was removed, t if anything was"
  (let* ((key-list (mapcar key elements))
	 (greatest (apply max key-list))
	 (least    (apply min key-list)))
    (if (eql greatest least)
	(values elements nil)
	(values (remove-if (lambda (elt)
			     (not (eql greatest (funcall key elt))))
			   elements)
		t))))

(defun unit-list-p (list)
  "Returns true if list is a list of one element"
  (and (consp list) (null (cdr list))))

(defun most-specific-method (methods)
  (declare (type cons methods))
  (if (unit-list-p methods)
      methods
      (let ((methods (remove-not-greatest methods (lambda (method) ; # required parameters
						    (length (method-required-parameters method))))))
	(if (unit-list-p methods)
	    methods
	    (let ((methods (remove-not-greatest methods (lambda (method) ; # specialized required parameters
							  (length (remove-if (complement #'specialized-p)
									     (method-required-parameters method)))))))
	      (if (unit-list-p methods)
		  methods
		  (let ((methods (remove-not-greatest methods (lambda (method) ; # key parameters
								(length (method-key-parameters method))))))
		    methods)))))))
	

(defun select-url-method (handler request-method request urlmethod-name)
  "Given a handler, request-method (GET, POST, etc), request, and the name of the urlmethod, performs
specialization and calls the method"
  (declare (type handler handler)
	   (symbol request-method)
	   (type request request)
	   (ignore handler request-method))
  (let* ((urlparameters (mapcar (lambda (x)
				  (list (first x) (if (second x) (untaint #'identity (second x)) nil)))
				(tainted-url-query-alist (request-url request)))) ; call tainted to shut up warnings
	 (matching-methods (remove-if (lambda (method-info)
					(or (not (eql (method-method-name method-info) urlmethod-name))
					    (not (fulfills-specialization (method-required-parameters method-info) urlparameters))))
				      *url-methods*)))
    (if (null matching-methods)
	(error 'no-urlmethod :method-name urlmethod-name :parameters urlparameters)
	(let ((msm (most-specific-method matching-methods)))
	  (if (> (length msm) 1)
	      (error 'too-many-urlmethods-matched :matched-methods msm :method-name urlmethod-name :parameters urlparameters)
	      (first msm))))))

(defun call-url-method (method-info handler request-method request function-parameter-values)
  (let* ((tainted-parameters (tainted-url-query-alist (request-url request)))
	 (parameter-values (mapcar (lambda (x)
				     (let ((value (second (assoc x tainted-parameters :test #'string-equal))))
				       (if value
					   value
					   (let ((key-parameter (find x
								      (method-key-parameters method-info)
								      :key #'parameter-name-from-key-parameter)))
					     (if key-parameter
						 (taint (default-value-for-key-parameter key-parameter))
						 nil)))))
				   (remove-if (lambda (param)
						(member param (method-function-parameters method-info)))
					      (method-all-parameters method-info)))))
    (ecase (method-type method-info)
      (:untainted (apply (method-lambda method-info) (list* handler request-method request
							    (append function-parameter-values
								    (mapcar (lambda (x) (if x (untaint #'identity x) nil)) parameter-values)))))
      (:tainted   (apply (method-lambda method-info) (list* handler request-method request
							    (append function-parameter-values
								    parameter-values)))))))
      
(defun specialized-p (parameter)
  (consp parameter))
(defun defaulted-p (parameter)
  (consp parameter))
(defun default-value-for-key-parameter (parameter)
  (if (consp parameter)
      (second parameter)
      nil))

(defun parameter-name-from-url-specialization (place)
  (if (symbolp place)
      place
      (first place)))
(defun parameter-name-from-key-parameter (place)
  (if (consp place)
      (first place)
      place))
      

(defun equal-urlmethod-signature (signature-1 signature-2)
  (equal (subseq signature-1 0 6)
	 (subseq signature-2 0 6)))

(defun extract-parameters-for-urlmethod (parameters)
  (let ((function-parameters nil)
	(require-parameters nil)
	(key-parameters nil)
	(all-parameters nil)
	(current-elt :function))
    (dolist (i parameters)
      (if (and (symbolp i) (equal (elt (symbol-name i) 0) #\&))
	  (let ((iname (symbol-name i)))
	    (cond
	      ((string-equal iname "&REQUIRE") (setf current-elt :require))
	      ((string-equal iname "&KEY")     (setf current-elt :key))
	      (t (error 'urlmethod-unknown-keyword :unknown iname :lambda-list parameters))))
	  (progn
	    (ecase current-elt
	      (:function (push i function-parameters))
	      (:require  (push i require-parameters))
	      (:key      (push i key-parameters)))
	    (push (parameter-name-from-url-specialization i) all-parameters))))
    (values (reverse function-parameters)
	    (reverse require-parameters)
	    (reverse key-parameters)
	    (reverse all-parameters))))

(defun define-urlmethod-function (method-name handlersym methodsym requestsym parameters)
  (unless (find method-name *url-methods-seen*)
    (push method-name *url-methods-seen*)
    (multiple-value-bind (function-parameters require-parameters key-parameters all-parameters) (extract-parameters-for-urlmethod parameters)
      (declare (ignore require-parameters key-parameters all-parameters))
      `(defun ,method-name (,handlersym ,methodsym ,requestsym ,@function-parameters)
	(araneida::call-url-method (araneida::select-url-method ,handlersym ,methodsym ,requestsym ',method-name)
	 ,handlersym ,methodsym ,requestsym (list ,@function-parameters))))))

(defun create-urlmethod (type method-name handlersym methodsym requestsym parameters body)
  (multiple-value-bind (function-parameters require-parameters key-parameters all-parameters) (extract-parameters-for-urlmethod parameters)
    (with-gensyms (method-info conflicting-signature-pos length-function-parameters methods-of-the-same-name)
      `(let* ((,method-info (list
			     ',method-name ,(ecase type (:tainted :tainted) (:untainted :untainted))
			     ',function-parameters
			     ',require-parameters ',key-parameters
			     ',all-parameters (lambda (,handlersym ,methodsym ,requestsym ,@all-parameters)
						  ,@body)))
	      (,conflicting-signature-pos (position-if (lambda (elt)
							 (araneida::equal-urlmethod-signature elt
											      ,method-info))
						       araneida::*url-methods*))
	      (,length-function-parameters ,(length function-parameters))
	      (,methods-of-the-same-name (remove-if (lambda (method)
						      (not (eql (araneida::method-method-name method) ',method-name)))
						    araneida::*url-methods*)))
	(unless (zerop (length ,methods-of-the-same-name))
	  (when (some (lambda (method)
			(not (equal (length (araneida::method-function-parameters method)) ,length-function-parameters)))
		      ,methods-of-the-same-name)
	    (error 'araneida::urlmethod-function-parameter-mismatch
		   :parameters ',parameters
		   :other-methods-info ',methods-of-the-same-name)))
	(if ,conflicting-signature-pos
	    (setf (elt araneida::*url-methods* ,conflicting-signature-pos) ,method-info)
	    (pushnew ,method-info *url-methods*))))))

;FIXME: add specialization for methodsym
(defmacro defurlmethod (method-name (handlersym methodsym requestsym &rest parameters) &body body)
  "Define a urlmehod. This is like a CLOS method, but the parameters are extracted from the url (eventually will work with POST as well)
The syntax is very similar to defmethod.

(defurlmethod method-name (handler-symbol method-symbol request-symbol [function-parameter...] [&require require-parameter...] [&key key-parameter...])
  [docstring]
  body)

*-symbol: names which will be bound to the handler, method, and request
function-parameter -- when the method is called like so (method-name handler method request a b c...), a b c are function-parameters
require-parameter: parameter-name  -- requires that the parameter named be present. Parameter will be bound to this name
                 | (parameter-name (function [function-parameter...]) -- requires that parameter be present and that it satisfy
                                                                        (apply #'function parameter-value function-parameters)

key-parameter: parameter-name -- bind the value of the parameter (if any) to this name. nil if no value
             | (parameter-name parameter-default-value) -- binds the value to parameter name, or parameter-default-value if no value

Please note that parameter-name is matched to parameters without regard to case.
There are numerous examples in test-server.lisp

Precendence algorithm is as follows:
Given a list of methods that all satisfy the current call, return the most specific one(s).
The algorithm for this is a bit different than for standard generic methods.

(unimplemented) If any have a specialized handler, they take precedence over unspecialized
(unimplemented) Of specialized handlers, class precedence is as for standard CLOS
(unimplemented) Specialized request-method takes precedence over unspecialized
 Methods with more required parameters take precedence over those with fewer
 Methods with more specialized required parameters take precedence over those with fewer
 Methods with more key parameters take precedence over those with fewer
 Tainted methods take precedence over untainted"
  `(progn
    ,(define-urlmethod-function method-name handlersym methodsym requestsym parameters)
    ,(create-urlmethod :untainted method-name handlersym methodsym requestsym parameters body)))

(defmacro deftaintedurlmethod (method-name (handlersym methodsym requestsym &rest parameters) &body body)
  "Just like defurlmethod, except the parameter values will be tainted"
  `(progn
    ,(define-urlmethod-function method-name handlersym methodsym requestsym parameters)
    ,(create-urlmethod :tainted method-name handlersym methodsym requestsym parameters body)))