Christophe Weblog Wiki Code Publications Music
rearrange repository to have src/ and examples/ directories
[specializable.git] / examples / accept-specializer.lisp
diff --git a/examples/accept-specializer.lisp b/examples/accept-specializer.lisp
new file mode 100644 (file)
index 0000000..c08f537
--- /dev/null
@@ -0,0 +1,268 @@
+(in-package "SPECIALIZABLE")
+
+(defstruct accept-node
+  (name (error "missing name"))
+  (children nil)
+  (q nil))
+(defun print-accept-tree (tree stream)
+  (let (*stack*)
+    (declare (special *stack*))
+    (labels ((walk (fun node)
+               (let ((*stack* (cons node *stack*)))
+                 (declare (special *stack*))
+                 (mapc (lambda (x) (walk fun x)) (accept-node-children node)))
+               (funcall fun node))
+             (stringify (node)
+               (case (length *stack*)
+                 (0 "*/*")
+                 (1 (format nil "~A/*" (accept-node-name node)))
+                 (2 (format nil "~A/~A" (accept-node-name (car *stack*)) (accept-node-name node))))))
+      (let ((first t))
+        (walk
+         (lambda (x)
+           (let ((q (accept-node-q x)))
+             (when q
+               (format stream "~:[, ~;~]" first)
+               (format stream "~A~:[;q=~A~;~]" (stringify x) (= q 1.0) q)
+               (setf first nil))))
+         tree)))))
+(defmethod print-object ((o accept-node) s)
+  (if (accept-node-name o)
+      (call-next-method)
+      (pprint-logical-block (s nil)
+        (print-unreadable-object (o s :type t)
+          (print-accept-tree o s)))))
+
+(defun q (media-type accept-tree)
+  (let* ((pos (position #\/ media-type))
+         (type (subseq media-type 0 pos))
+         (subtype (subseq media-type (1+ pos)))
+         (type-node (find type (accept-node-children accept-tree) :key #'accept-node-name :test #'string=))
+         (subtype-node (and type-node (find subtype (accept-node-children type-node) :key #'accept-node-name :test #'string=))))
+    (or (and subtype-node (accept-node-q subtype-node))
+        (and type-node (accept-node-q type-node))
+        (accept-node-q accept-tree))))
+
+(defun q-ok (media-type accept-tree)
+  (let ((q (q media-type accept-tree)))
+    (and q (> q 0) q)))
+
+(defun insert (range q tree)
+  (labels ((ensure-node (range tree)
+             (cond
+               ((null range) tree)
+               (t (ensure-node (cdr range)
+                               (or (find (car range) (accept-node-children tree)
+                                         :key #'accept-node-name :test #'string=)
+                                   (car (push
+                                         (make-accept-node :name (car range))
+                                         (accept-node-children tree)))))))))
+    (let ((node (ensure-node range tree)))
+      ;; we could choose different behaviour here
+      (setf (accept-node-q node) q))
+    tree))
+
+(defun parse-accept-string (string)
+  (flet ((whitespacep (x)
+           (member x '(#\Space #\Tab))))
+    (let ((string (remove-if #'whitespacep string))
+          (result (make-accept-node :name nil)))
+      (cl-ppcre:do-register-groups (type subtype qp q)
+          ;; not desperately error-proof
+          ("([a-z]*|\\*)/([a-z0-9]*|\\*)(;q=([01]\\.[0-9]*))?(,|$)" string result)
+        (if qp
+            (setf q (float (+ (digit-char-p (char q 0))
+                              (/ (parse-integer q :start 2)
+                                 (expt 10 (- (length q) 2))))))
+            (setf q 1.0))
+        (let ((range (and (string/= type "*")
+                          (cons type (and (string/= subtype "*")
+                                          (list subtype))))))
+          (insert range q result))))))
+\f
+;;; FIXME: tiebreaker predicate (maybe defaulting to string<)?
+(defclass accept-specializer (extended-specializer)
+  ((media-type :initarg :media-type :type string :reader media-type)))
+(defmethod print-object ((o accept-specializer) s)
+  (print-unreadable-object (o s :type t)
+    (format s "~S" (media-type o))))
+;;; FIXME: would be cute to have sb-pcl:generalizer to inherit from.
+;;; Or maybe specializable:extended-generalizer could handle the NEXT
+;;; functionality?
+(defclass accept-generalizer ()
+  ((header :initarg :header :reader header)
+   (tree)
+   (next :initarg :next :reader next)))
+(defmethod print-object ((o accept-generalizer) s)
+  (print-unreadable-object (o s :type t)
+    (print-accept-tree (tree o) s)))
+(defmethod tree ((x accept-generalizer))
+  (if (slot-boundp x 'tree)
+      (slot-value x 'tree)
+      (setf (slot-value x 'tree) (parse-accept-string (header x)))))
+(defclass accept-generic-function (specializable-generic-function)
+  ()
+  (:metaclass sb-mop:funcallable-standard-class))
+
+(define-extended-specializer accept (gf arg)
+  (declare (ignore gf))
+  (make-instance 'accept-specializer :media-type arg))
+(defmethod sb-pcl:unparse-specializer-using-class
+    ((gf accept-generic-function) (specializer accept-specializer))
+  `(accept ,(media-type specializer)))
+(defmethod sb-pcl::same-specializer-p
+    ((s1 accept-specializer) (s2 accept-specializer))
+  (string= (media-type s1) (media-type s2)))
+
+(defmethod generalizer-of-using-class ((gf accept-generic-function) (arg tbnl:request))
+  (make-instance 'accept-generalizer
+                 :header (tbnl:header-in :accept arg)
+                 :next (call-next-method)))
+(defmethod generalizer-equal-hash-key
+    ((gf accept-generic-function) (g accept-generalizer))
+  `(accept-generalizer ,(header g)))
+(defmethod specializer-accepts-generalizer-p ((gf accept-generic-function) (s accept-specializer) (generalizer accept-generalizer))
+  (values (q-ok (media-type s) (tree generalizer)) t))
+(defmethod specializer-accepts-generalizer-p ((gf accept-generic-function) (s accept-specializer) generalizer)
+  (values nil t))
+(defmethod specializer-accepts-generalizer-p ((gf accept-generic-function) (s sb-mop:specializer) (generalizer accept-generalizer))
+  (specializer-accepts-generalizer-p gf s (next generalizer)))
+
+(defmethod specializer-accepts-p ((specializer accept-specializer) obj)
+  nil)
+(defmethod specializer-accepts-p ((specializer accept-specializer) (obj tbnl:request))
+  (q-ok (media-type specializer) (parse-accept-string (tbnl:header-in :accept obj))))
+
+(defmethod specializer< ((gf accept-generic-function) (s1 accept-specializer) (s2 accept-specializer) generalizer)
+  (cond
+    ((string= (media-type s1) (media-type s2)) '=)
+    (t (let ((q1 (q (media-type s1) (tree generalizer)))
+             (q2 (q (media-type s2) (tree generalizer))))
+         (cond
+           ((= q1 q2) '=)
+           ((< q1 q2) '>)
+           (t '<))))))
+(defmethod specializer< ((gf accept-generic-function) (s1 accept-specializer) (s2 class) generalizer)
+  '<)
+(defmethod specializer< ((gf accept-generic-function) (s1 accept-specializer) (s2 sb-mop:eql-specializer) generalizer)
+  '>)
+(defmethod specializer< ((gf accept-generic-function) (s1 sb-mop:specializer) (s2 accept-specializer) generalizer)
+  (ecase (specializer< gf s2 s1 generalizer)
+    ((>) '<)
+    ((<) '>)))
+(defmethod specializer< ((gf accept-generic-function) (s1 sb-mop:specializer) (s2 sb-mop:specializer) (g accept-generalizer))
+  (specializer< gf s1 s2 (next g)))
+\f
+(defvar *actual-content-type*)
+(defgeneric handle-content-type (x))
+(define-method-combination content-negotiation ()
+  ((around (:around))
+   (before (:before))
+   (primary () :required t)
+   (after (:after)))
+  (:arguments request)
+  (labels ((call-methods (methods)
+             (mapcar #'(lambda (method)
+                         `(call-method ,method))
+                     methods))
+           (transform (primaries)
+             (let ((method (car primaries))
+                   (nexts (cdr primaries)))
+               `(make-method
+                 (progn
+                   (let ((request-specializer (car (sb-mop:method-specializers ,method))))
+                     (when (typep request-specializer 'accept-specializer)
+                       (setf *actual-content-type* (media-type request-specializer))))
+                   (throw 'content-negotiation (call-method ,method ,@(and nexts `((,(transform nexts))))))))))
+           (wrap (form)
+             `(let ((*actual-content-type*))
+                (multiple-value-prog1
+                    ,form
+                  (handle-content-type ,request)))))
+    (let ((form (if (or before after (rest primary))
+                    `(multiple-value-prog1
+                         (progn ,@(call-methods before)
+                                (catch 'content-negotiation (call-method ,(transform primary))))
+                       ,@(call-methods (reverse after)))
+                    `(catch 'content-negotiation (call-method ,(transform primary))))))
+      (if around
+          (wrap `(call-method ,(first around)
+                              (,@(rest around) (make-method ,form))))
+          (wrap form)))))
+(define-method-combination content-negotiation/or ()
+  ((around (:around))
+   (primary () :required t))
+  (:arguments request)
+  (labels ((transform/1 (method)
+             `(make-method
+               (progn
+                 (let ((s (car (sb-mop:method-specializers ,method))))
+                   (when (typep s 'accept-specializer)
+                     (setf *actual-content-type* (media-type s))))
+                 (call-method ,method))))
+           (transform (primaries)
+             (mapcar #'(lambda (x) `(call-method ,(transform/1 x)))
+                     primaries))
+           (wrap (form)
+             `(let ((*actual-content-type*))
+                (multiple-value-prog1
+                    ,form
+                  (handle-content-type ,request)))))
+    (let ((form (if (rest primary)
+                    `(or ,@(transform primary))
+                    `(call-method ,(transform/1 (car primary))))))
+      (if around
+          (wrap `(call-method ,(first around)
+                              (,@(rest around) (make-method ,form))))
+          (wrap form)))))
+
+(defmethod generalizer-of-using-class ((gf accept-generic-function) (s string))
+  (make-instance 'accept-generalizer
+                 :header s
+                 :next (call-next-method)))
+(defmethod specializer-accepts-p ((s accept-specializer) (string string))
+  (q-ok (media-type s) (parse-accept-string string)))
+
+(defmethod handle-content-type ((x tbnl:request))
+  (setf (tbnl:content-type*) *actual-content-type*))
+(defmethod handle-content-type ((x string))
+  (format t "~&Content-Type: ~A" *actual-content-type*))
+
+(defgeneric respond (request)
+  (:generic-function-class accept-generic-function)
+  (:method-combination list))
+(defmethod respond list (request)
+  t)
+(defmethod respond list ((s string))
+  'string)
+(defmethod respond list ((s (accept "text/html")))
+  "text/html")
+(defmethod respond list ((s (accept "audio/mp3")))
+  "audio/mp3")
+
+(defgeneric cn-test (request)
+  (:generic-function-class accept-generic-function)
+  (:method-combination content-negotiation))
+(defmethod cn-test ((request (accept "text/html")))
+  'html)
+(defmethod cn-test ((request (accept "text/plain")))
+  'plain)
+(defmethod cn-test ((request (accept "image/webp")))
+  'webp)
+(defmethod cn-test ((request (accept "audio/mp3")))
+  (call-next-method)
+  'mp3)
+(defmethod cn-test :after (request)
+  (print 'after))
+
+(defgeneric cn/or-test (request)
+  (:generic-function-class accept-generic-function)
+  (:method-combination content-negotiation/or))
+
+(defmethod cn/or-test or ((request (accept "audio/mp3")))
+  'mp3)
+(defmethod cn/or-test or ((request (accept "image/webp")))
+  'webp)
+(defmethod cn/or-test :around ((request t))
+  (print :around)
+  (call-next-method))