Browse code
misc
Ed Langley authored on 29/10/2020 23:58:30
Showing 6 changed files
Showing 6 changed files
- db.lisp
- mc-web.lisp
- myway-acceptor.lisp
- server-aware-class.lisp
- web-test.lisp
- websocket-acceptor.lisp
1 | 1 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,75 @@ |
1 |
+(defpackage :fwoar.web-test.db |
|
2 |
+ (:use :cl ) |
|
3 |
+ (:export :blog-post :id :title :author :content |
|
4 |
+ :make-blog-post |
|
5 |
+ :save-blog-post |
|
6 |
+ :blog)) |
|
7 |
+(in-package :fwoar.web-test.db) |
|
8 |
+ |
|
9 |
+(defgeneric id (ided-object)) |
|
10 |
+(defgeneric title (title-object)) |
|
11 |
+(defgeneric author (author-object)) |
|
12 |
+(defgeneric content (content-object)) |
|
13 |
+ |
|
14 |
+(defclass ided () |
|
15 |
+ ((%id :initarg :id :reader id))) |
|
16 |
+ |
|
17 |
+(defun slugify (v) |
|
18 |
+ (format nil "~(~{~a~^-~}~)" |
|
19 |
+ (coerce (fwoar.string-utils:split " " v) |
|
20 |
+ 'list))) |
|
21 |
+ |
|
22 |
+(defclass blog-post (ided) |
|
23 |
+ ((title :initarg :title :reader title :initform (error "must have a title")) |
|
24 |
+ (author :initarg :author :reader author :initform (error "must have a author")) |
|
25 |
+ (content :initarg :content :reader content :initform (error "must have a content"))) |
|
26 |
+ (:metaclass fwoar.server-aware-class:server-aware-class)) |
|
27 |
+ |
|
28 |
+(defmethod slot-unbound (class (blog-post blog-post) (slot-name (eql '%id))) |
|
29 |
+ (setf (slot-value blog-post slot-name) |
|
30 |
+ (slugify (title blog-post)))) |
|
31 |
+ |
|
32 |
+(defmethod yason:encode-slots progn ((object blog-post)) |
|
33 |
+ (yason:encode-object-elements |
|
34 |
+ "title" (title object) |
|
35 |
+ "author" (author object) |
|
36 |
+ "content" (content object))) |
|
37 |
+ |
|
38 |
+(defmacro new (class &rest initializer-syms) |
|
39 |
+ `(make-instance ,class |
|
40 |
+ ,@(mapcan (serapeum:op (list (alexandria:make-keyword _1) _1)) |
|
41 |
+ initializer-syms))) |
|
42 |
+ |
|
43 |
+(defun make-blog-post (title author content) |
|
44 |
+ (new 'blog-post title author content)) |
|
45 |
+ |
|
46 |
+(defun save-blog-post (blog post) |
|
47 |
+ (montezuma:add-document-to-index blog post)) |
|
48 |
+ |
|
49 |
+ |
|
50 |
+(defmethod fset:compare ((a blog-post) (b blog-post)) |
|
51 |
+ (fset:compare-slots a b 'title 'author 'content)) |
|
52 |
+ |
|
53 |
+(defclass blog (montezuma:index) |
|
54 |
+ ()) |
|
55 |
+ |
|
56 |
+(defmethod montezuma:add-document-to-index ((index blog) (doc blog-post) |
|
57 |
+ &optional analyzer) |
|
58 |
+ (with-accessors ((title title) (author author) (content content)) doc |
|
59 |
+ (let* ((doc (make-instance 'montezuma:document))) |
|
60 |
+ (montezuma:add-field doc (montezuma:make-field "title" title)) |
|
61 |
+ (montezuma:add-field doc (montezuma:make-field "author" author)) |
|
62 |
+ (montezuma:add-field doc (montezuma:make-field "content" content |
|
63 |
+ :index :tokenized)) |
|
64 |
+ (montezuma:add-document-to-index index doc analyzer)))) |
|
65 |
+ |
|
66 |
+(defmethod montezuma:get-document ((index blog) doc-number) |
|
67 |
+ (let ((doc (call-next-method))) |
|
68 |
+ (funcall (alexandria:compose |
|
69 |
+ (data-lens:applying 'make-blog-post) |
|
70 |
+ (data-lens:over 'montezuma:field-data) |
|
71 |
+ (data-lens:juxt |
|
72 |
+ (lambda (it) (montezuma:document-field it "title")) |
|
73 |
+ (lambda (it) (montezuma:document-field it "author")) |
|
74 |
+ (lambda (it) (montezuma:document-field it "content")))) |
|
75 |
+ doc))) |
0 | 76 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,58 @@ |
1 |
+(defpackage :fwoar.mc-web |
|
2 |
+ (:use :cl) |
|
3 |
+ (:export |
|
4 |
+ #:myway-server |
|
5 |
+ #:*current-route* |
|
6 |
+ #:routing |
|
7 |
+ #:define-route-group |
|
8 |
+ #:resolve-request)) |
|
9 |
+(in-package :fwoar.mc-web) |
|
10 |
+ |
|
11 |
+(defclass myway-acceptor (hunchentoot:acceptor) |
|
12 |
+ ((%mapper :initform (myway:make-mapper) :reader mapper))) |
|
13 |
+ |
|
14 |
+(defvar *current-route*) |
|
15 |
+(define-method-combination route-group |
|
16 |
+ () |
|
17 |
+ ((routes (:route *))) |
|
18 |
+ (:arguments server) |
|
19 |
+ `(progn |
|
20 |
+ ,@(mapcar (lambda (method) |
|
21 |
+ `(let ((*method-route* ,(cadr (method-qualifiers method)))) |
|
22 |
+ (myway:connect (mapper ,server) *method-route* |
|
23 |
+ (call-method ,method)))) |
|
24 |
+ (stable-sort routes #'< |
|
25 |
+ :key (alexandria:compose #'length |
|
26 |
+ #'cadr |
|
27 |
+ #'method-qualifiers))))) |
|
28 |
+ |
|
29 |
+(defmacro define-route-group (name (&rest args) &body body) |
|
30 |
+ (let* ((docstring (when (and body (stringp (car body))) |
|
31 |
+ (car body))) |
|
32 |
+ (body (if docstring |
|
33 |
+ (cdr body) |
|
34 |
+ body))) |
|
35 |
+ (alexandria:with-gensyms (server) |
|
36 |
+ `(defgeneric ,name (,server ,@args) |
|
37 |
+ (:method-combination routing) |
|
38 |
+ (:documentation ,docstring) |
|
39 |
+ ,@(mapcar (serapeum:op `(:method |
|
40 |
+ ,@(subseq _1 0 2) |
|
41 |
+ (,server ,@(elt _1 2)) |
|
42 |
+ ,@(subseq _1 3))) |
|
43 |
+ body))))) |
|
44 |
+ |
|
45 |
+(defgeneric resolve-request (acceptor request) |
|
46 |
+ (:method-combination or :most-specific-first) |
|
47 |
+ (:method or ((acceptor myway-acceptor) request) |
|
48 |
+ (setf (hunchentoot:return-code*) 404) |
|
49 |
+ (format nil "~s not found" (hunchentoot:script-name*)))) |
|
50 |
+ |
|
51 |
+(defmethod hunchentoot:acceptor-dispatch-request ((acceptor myway-acceptor) request) |
|
52 |
+ (let ((router (mapper acceptor))) |
|
53 |
+ (multiple-value-bind (result matched) (myway:dispatch router (hunchentoot:script-name*) |
|
54 |
+ :method (hunchentoot:request-method*)) |
|
55 |
+ (if matched |
|
56 |
+ result |
|
57 |
+ (or (resolve-request acceptor request) |
|
58 |
+ (call-next-method)))))) |
0 | 59 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,63 @@ |
1 |
+(defpackage :fwoar.myway-acceptor |
|
2 |
+ (:use :cl) |
|
3 |
+ (:export |
|
4 |
+ #:*current-route* |
|
5 |
+ #:define-route-group |
|
6 |
+ #:resolve-request |
|
7 |
+ #:myway-acceptor |
|
8 |
+ #:route-group)) |
|
9 |
+(in-package :fwoar.myway-acceptor) |
|
10 |
+ |
|
11 |
+(defclass myway-acceptor (hunchentoot:acceptor) |
|
12 |
+ ((%mapper :initform (myway:make-mapper) :reader mapper))) |
|
13 |
+ |
|
14 |
+(defvar *current-route*) |
|
15 |
+(define-method-combination route-group |
|
16 |
+ () |
|
17 |
+ ((routes (:route . *))) |
|
18 |
+ (:arguments server) |
|
19 |
+ `(progn |
|
20 |
+ ,@(mapcar (lambda (method) |
|
21 |
+ (destructuring-bind (_ path . rest) (method-qualifiers method) |
|
22 |
+ (declare (ignore _)) |
|
23 |
+ (let ((http-verb (when rest |
|
24 |
+ (etypecase (car rest) |
|
25 |
+ (keyword (list :method (car rest))))))) |
|
26 |
+ `(let ((*current-route* ,path)) |
|
27 |
+ (myway:connect (mapper ,server) ,path |
|
28 |
+ (call-method ,method) |
|
29 |
+ ,@http-verb))))) |
|
30 |
+ (stable-sort routes #'< |
|
31 |
+ :key (alexandria:compose #'length |
|
32 |
+ #'cadr |
|
33 |
+ #'method-qualifiers))))) |
|
34 |
+ |
|
35 |
+(defmacro define-route-group (name (server &rest args) &body body) |
|
36 |
+ (let* ((docstring (when (and body (stringp (car body))) |
|
37 |
+ (car body))) |
|
38 |
+ (body (if docstring |
|
39 |
+ (cdr body) |
|
40 |
+ body))) |
|
41 |
+ `(defgeneric ,name (,server ,@args) |
|
42 |
+ (:method-combination route-group) |
|
43 |
+ (:documentation ,docstring) |
|
44 |
+ ,@(mapcar (serapeum:op `(:method |
|
45 |
+ ,@(subseq _1 0 2) |
|
46 |
+ (,@(elt _1 2)) |
|
47 |
+ ,@(subseq _1 3))) |
|
48 |
+ body)))) |
|
49 |
+ |
|
50 |
+(defgeneric resolve-request (acceptor request) |
|
51 |
+ (:method-combination or :most-specific-first) |
|
52 |
+ (:method or ((acceptor myway-acceptor) request) |
|
53 |
+ (setf (hunchentoot:return-code*) 404) |
|
54 |
+ (format nil "~s not found" (hunchentoot:script-name*)))) |
|
55 |
+ |
|
56 |
+(defmethod hunchentoot:acceptor-dispatch-request ((acceptor myway-acceptor) request) |
|
57 |
+ (let ((router (mapper acceptor))) |
|
58 |
+ (multiple-value-bind (result matched) (myway:dispatch router (hunchentoot:script-name*) |
|
59 |
+ :method (hunchentoot:request-method*)) |
|
60 |
+ (if matched |
|
61 |
+ result |
|
62 |
+ (or (resolve-request acceptor request) |
|
63 |
+ (call-next-method)))))) |
0 | 64 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,28 @@ |
1 |
+(defpackage :fwoar.server-aware-class |
|
2 |
+ (:use :cl ) |
|
3 |
+ (:export |
|
4 |
+ #:server-aware-class |
|
5 |
+ #:serializable |
|
6 |
+ #:publish-value |
|
7 |
+ #:server)) |
|
8 |
+(in-package :fwoar.server-aware-class) |
|
9 |
+ |
|
10 |
+(defgeneric publish-value (server class slot old-value new-value) |
|
11 |
+ ) |
|
12 |
+ |
|
13 |
+(defclass server-aware-class (standard-class) |
|
14 |
+ ((%server :accessor server :initform nil))) |
|
15 |
+ |
|
16 |
+(defclass serializable () |
|
17 |
+ ()) |
|
18 |
+ |
|
19 |
+(defmethod closer-mop:validate-superclass ((meta server-aware-class) (class standard-class)) |
|
20 |
+ t) |
|
21 |
+ |
|
22 |
+(defmethod (setf closer-mop:slot-value-using-class) :around (new-value (class server-aware-class) object slotd) |
|
23 |
+ (if (and (server class) (c2mop:slot-boundp-using-class class object slotd)) |
|
24 |
+ (let ((old-value (c2mop:slot-value-using-class class object slotd))) |
|
25 |
+ (call-next-method) |
|
26 |
+ (publish-value (server class) (closer-mop:class-prototype class) (closer-mop:slot-definition-name slotd) |
|
27 |
+ old-value new-value)) |
|
28 |
+ (call-next-method))) |
... | ... |
@@ -1,72 +1,49 @@ |
1 | 1 |
(defpackage :fwoar.web-test |
2 |
+ (:import-from :fwoar.myway-acceptor :define-route-group) |
|
3 |
+ (:local-nicknames |
|
4 |
+ (:db :fwoar.web-test.db)) |
|
2 | 5 |
(:use :cl ) |
3 | 6 |
(:export )) |
4 | 7 |
(in-package :fwoar.web-test) |
5 | 8 |
|
6 |
-(defclass websocket-server (hunchensocket:websocket-acceptor |
|
7 |
- hunchentoot:easy-acceptor) |
|
8 |
- ()) |
|
9 |
+(defclass my-server (fwoar.websocket-acceptor:websocket-acceptor |
|
10 |
+ fwoar.myway-acceptor:myway-acceptor) |
|
11 |
+ ((%models :initarg :models :reader models))) |
|
12 |
+ |
|
13 |
+(defgeneric register-model (server model) |
|
14 |
+ (:method (server model) |
|
15 |
+ (error "~s is not a class or a class name" model)) |
|
16 |
+ (:method (server (model standard-class)) |
|
17 |
+ (error "can only register server-aware classes, got ~s!" model)) |
|
18 |
+ (:method ((server my-server) (model fwoar.server-aware-class:server-aware-class)) |
|
19 |
+ (setf (fwoar.server-aware-class:server model) server)) |
|
20 |
+ (:method ((server my-server) (model symbol)) |
|
21 |
+ (register-model server (find-class model)))) |
|
9 | 22 |
|
10 |
-(defgeneric find-websocket-resource (acceptor request)) |
|
11 |
- |
|
12 |
-(defmethod hunchentoot:acceptor-dispatch-request ((acceptor websocket-server) |
|
13 |
- (request hunchensocket::websocket-request)) |
|
14 |
- "Attempt WebSocket connection, else fall back to HTTP" |
|
15 |
- (cond ((and (member "upgrade" (cl-ppcre:split "\\s*,\\s*" (hunchentoot:header-in* :connection)) |
|
16 |
- :test #'string-equal) |
|
17 |
- (string= "websocket" (string-downcase (hunchentoot:header-in* :upgrade)))) |
|
18 |
- (cond ((setf (hunchensocket:websocket-resource hunchentoot:*request*) |
|
19 |
- (find-websocket-resource acceptor hunchentoot:*request*)) |
|
20 |
- ;; Found the websocket resource |
|
21 |
- (hunchensocket::handle-handshake acceptor hunchentoot:*request* hunchentoot:*reply*) |
|
22 |
- ;; HACK! the empty string is also important because if there's |
|
23 |
- ;; no content Hunchentoot will declare the connection closed and |
|
24 |
- ;; set "Connection: Closed". But there can't be any actual |
|
25 |
- ;; content since otherwise it will piggyback onto the first |
|
26 |
- ;; websocket frame, which is interpreted as invalid by the |
|
27 |
- ;; client. It's also forbidden by the HTTP RFC2616: |
|
28 |
- ;; |
|
29 |
- ;; [...] All 1xx (informational), 204 (no content), and 304 |
|
30 |
- ;; (not modified) responses MUST NOT include a |
|
31 |
- ;; message-body. [...] |
|
32 |
- ;; |
|
33 |
- ;; There is a slight non-conformance here: this trick makes |
|
34 |
- ;; Hunchentoot send "Content-length: 0". Most browsers don't |
|
35 |
- ;; seem to care, but RFC2616 kind of implies that is forbidden, |
|
36 |
- ;; since it says the |
|
37 |
- ;; |
|
38 |
- ;; [...] the presence of a message-body is signaled by the |
|
39 |
- ;; inclusion of a Content-Length or Transfer-Encoding header |
|
40 |
- ;; field in the requests's message-headers [...] |
|
41 |
- ;; |
|
42 |
- ;; Note however, we're sending a response, not a request. |
|
43 |
- ;; |
|
44 |
- (values "" nil nil)) |
|
45 |
- (t |
|
46 |
- ;; Didn't find the websocket-specific resource, return 404. |
|
47 |
- (setf (hunchentoot:return-code hunchentoot:*reply*) |
|
48 |
- hunchentoot:+http-not-found+) |
|
49 |
- (values nil nil nil)))) |
|
50 |
- (t |
|
51 |
- ;; Client is not requesting websockets, let Hunchentoot do its HTTP |
|
52 |
- ;; thing undisturbed. |
|
53 |
- (call-next-method)))) |
|
54 |
- |
|
55 |
-(defclass my-server (websocket-server) |
|
23 |
+(defclass console-publisher () |
|
56 | 24 |
()) |
57 | 25 |
|
58 |
-(defclass chat-room (hunchensocket:websocket-resource) |
|
59 |
- ((name :initarg :name :initform (error "Name this room!") :reader name)) |
|
60 |
- (:default-initargs :client-class 'user)) |
|
26 |
+(defmethod register-model ((publisher console-publisher) |
|
27 |
+ (model fwoar.server-aware-class:server-aware-class)) |
|
28 |
+ (setf (fwoar.server-aware-class:server model) |
|
29 |
+ publisher)) |
|
30 |
+ |
|
31 |
+(defmethod update-instance-for-redefined-class ((instance my-server) added-slots discarded-slots property-list &key models) |
|
32 |
+ (when (member '%models added-slots) |
|
33 |
+ (setf (slot-value instance '%models) models))) |
|
61 | 34 |
|
62 | 35 |
(defclass user (hunchensocket:websocket-client) |
63 | 36 |
((name :initarg :user-agent :reader name :initform (error "Name this user!")))) |
64 | 37 |
|
38 |
+(defclass chat-room (hunchensocket:websocket-resource) |
|
39 |
+ ((name :initarg :name :initform (error "Name this room!") :reader name)) |
|
40 |
+ (:default-initargs :client-class 'user)) |
|
41 |
+ |
|
65 | 42 |
(defvar *chat-rooms* |
66 | 43 |
(list (make-instance 'chat-room :name "/bongo") |
67 | 44 |
(make-instance 'chat-room :name "/fury"))) |
68 | 45 |
|
69 |
-(defmethod find-websocket-resource ((acceptor my-server) request) |
|
46 |
+(defmethod fwoar.websocket-acceptor:find-websocket-resource ((acceptor my-server) request) |
|
70 | 47 |
(find (hunchentoot:script-name request) |
71 | 48 |
*chat-rooms* :test #'string= :key #'name)) |
72 | 49 |
|
... | ... |
@@ -75,10 +52,122 @@ |
75 | 52 |
do (hunchensocket:send-text-message peer (apply #'format nil message args)))) |
76 | 53 |
|
77 | 54 |
(defmethod hunchensocket:client-connected ((room chat-room) user) |
78 |
- (broadcast room "~a has joined ~a" (name user) (name room))) |
|
55 |
+ ) |
|
79 | 56 |
|
80 | 57 |
(defmethod hunchensocket:client-disconnected ((room chat-room) user) |
81 |
- (broadcast room "~a has left ~a" (name user) (name room))) |
|
58 |
+ ) |
|
82 | 59 |
|
83 | 60 |
(defmethod hunchensocket:text-message-received ((room chat-room) user message) |
84 |
- (broadcast room "~a says ~a" (name user) message)) |
|
61 |
+ ) |
|
62 |
+ |
|
63 |
+(defun render-post (post) |
|
64 |
+ (spinneret:with-html |
|
65 |
+ (:section |
|
66 |
+ (:header |
|
67 |
+ (:h* (db:title post)) |
|
68 |
+ (:div.author (db:author post))) |
|
69 |
+ (:section (db:content post))))) |
|
70 |
+ |
|
71 |
+(defun summarize (post) |
|
72 |
+ (spinneret:with-html |
|
73 |
+ (:li |
|
74 |
+ (:a :href (format nil "/post/~a" (db:id post)) |
|
75 |
+ (db:title post))))) |
|
76 |
+ |
|
77 |
+ |
|
78 |
+ |
|
79 |
+(defmethod fwoar.server-aware-class:publish-value ((server my-server) |
|
80 |
+ (object db:blog-post) |
|
81 |
+ (slot (eql 'posts)) |
|
82 |
+ (old-value fset:map) (new-value fset:map)) |
|
83 |
+ (unless (fset:equal? old-value new-value) |
|
84 |
+ (broadcast (car *chat-rooms*) |
|
85 |
+ (yason:with-output-to-string* (:indent t) |
|
86 |
+ (yason:with-object () |
|
87 |
+ (yason:encode-object-elements |
|
88 |
+ "slot" (string-downcase slot) |
|
89 |
+ "type" (string-downcase (class-name (class-of object)))) |
|
90 |
+ (yason:with-object-element ("value") |
|
91 |
+ (yason:with-array () |
|
92 |
+ (mapcar (lambda (it) |
|
93 |
+ (let ((val (fset:lookup new-value it))) |
|
94 |
+ (yason:with-object () |
|
95 |
+ (yason:encode-object-elements |
|
96 |
+ "id" (id val) |
|
97 |
+ "title" (title val))))) |
|
98 |
+ (fset:convert 'list (fset:domain (fset:map-difference-2 new-value old-value))))))))))) |
|
99 |
+ |
|
100 |
+(defmethod fwoar.server-aware-class:publish-value ((server console-publisher) |
|
101 |
+ (object db:blog-post) |
|
102 |
+ slot |
|
103 |
+ (old-value string) |
|
104 |
+ (new-value string)) |
|
105 |
+ (unless (equal old-value new-value) |
|
106 |
+ (format t "~&SLOT: ~s~%FROM: ~s~&TO: ~s~&" |
|
107 |
+ slot |
|
108 |
+ old-value |
|
109 |
+ new-value))) |
|
110 |
+ |
|
111 |
+ |
|
112 |
+(defgeneric m-index (thing) |
|
113 |
+ ) |
|
114 |
+ |
|
115 |
+(defclass blog () |
|
116 |
+ ((%montezuma-index :initarg :montezuma :reader m-index))) |
|
117 |
+ |
|
118 |
+(defun posts (blog) |
|
119 |
+ (declare (ignorable blog)) |
|
120 |
+ (list (db:make-blog-post "First Post" "Ed L" "This is the first post") |
|
121 |
+ (db:make-blog-post "Second Post" "Ed L" "This is the second post") |
|
122 |
+ (db:make-blog-post "Third Post" "Ed L" "This is the third post") |
|
123 |
+ (db:make-blog-post "Fourth Post" "Ed L" "This is the fourth post"))) |
|
124 |
+ |
|
125 |
+(defun connect-script () |
|
126 |
+ (ps:ps |
|
127 |
+ (defun summarize (post) |
|
128 |
+ (spinneret:with-html |
|
129 |
+ (:li |
|
130 |
+ (:a :href (ps:chain "/post/" |
|
131 |
+ (concat (ps:@ post id))) |
|
132 |
+ (ps:@ post title))))) |
|
133 |
+ (setf (ps:@ (ps:new (-web-socket "ws://localhost:12345/bongo")) onmessage) |
|
134 |
+ (lambda (e) |
|
135 |
+ (let ((data (ps:chain -j-s-o-n (parse (ps:@ e data))))) |
|
136 |
+ (ps:chain data value |
|
137 |
+ (map (lambda (x) |
|
138 |
+ (ps:chain document |
|
139 |
+ (query-selector "#posts") |
|
140 |
+ (append-child (summarize x))))))))))) |
|
141 |
+ |
|
142 |
+(defun page () |
|
143 |
+ (spinneret:with-html-string |
|
144 |
+ (:doctype) |
|
145 |
+ (:html |
|
146 |
+ (:body |
|
147 |
+ (:main |
|
148 |
+ (:section |
|
149 |
+ (:h* "My Blog") |
|
150 |
+ (:ul#posts |
|
151 |
+ (loop for post |
|
152 |
+ in (fset:convert 'list (fset:range (posts blog))) |
|
153 |
+ do (summarize post))) |
|
154 |
+ (:script |
|
155 |
+ (connect-script)))))))) |
|
156 |
+ |
|
157 |
+(define-route-group blog-methods (server blog) |
|
158 |
+ (:route "/" ((server my-server) (blog blog)) |
|
159 |
+ (lambda (params) |
|
160 |
+ (declare (ignore params)) |
|
161 |
+ )) |
|
162 |
+ (:route "/post/:id" ((server my-server) (blog blog)) |
|
163 |
+ (lambda (params) |
|
164 |
+ (spinneret:with-html-string |
|
165 |
+ (:doctype) |
|
166 |
+ (:html |
|
167 |
+ (:body |
|
168 |
+ (:main |
|
169 |
+ (:section |
|
170 |
+ (:h* "My Blog") |
|
171 |
+ (render-post |
|
172 |
+ (fset:lookup (posts blog) (getf params :id))))))))))) |
|
173 |
+ |
85 | 174 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,28 @@ |
1 |
+(defpackage :fwoar.websocket-acceptor |
|
2 |
+ (:use :cl ) |
|
3 |
+ (:export |
|
4 |
+ #:websocket-acceptor |
|
5 |
+ #:find-websocket-resource)) |
|
6 |
+(in-package :fwoar.websocket-acceptor) |
|
7 |
+ |
|
8 |
+(defclass websocket-acceptor (hunchensocket:websocket-acceptor) |
|
9 |
+ ()) |
|
10 |
+ |
|
11 |
+(defgeneric find-websocket-resource (acceptor request)) |
|
12 |
+ |
|
13 |
+(defmethod hunchentoot:acceptor-dispatch-request ((acceptor websocket-acceptor) |
|
14 |
+ (request hunchensocket::websocket-request)) |
|
15 |
+ "Attempt WebSocket connection, else fall back to HTTP. Override |
|
16 |
+version in hunchensocket to provide a better route-finding experience" |
|
17 |
+ (cond ((and (member "upgrade" (cl-ppcre:split "\\s*,\\s*" (hunchentoot:header-in* :connection)) |
|
18 |
+ :test #'string-equal) |
|
19 |
+ (string= "websocket" (string-downcase (hunchentoot:header-in* :upgrade)))) |
|
20 |
+ (cond ((setf (hunchensocket:websocket-resource hunchentoot:*request*) |
|
21 |
+ (find-websocket-resource acceptor hunchentoot:*request*)) |
|
22 |
+ (hunchensocket::handle-handshake acceptor hunchentoot:*request* hunchentoot:*reply*) |
|
23 |
+ (values "" nil nil)) |
|
24 |
+ (t ;; Didn't find the websocket-specific resource, return 404. |
|
25 |
+ (setf (hunchentoot:return-code hunchentoot:*reply*) |
|
26 |
+ hunchentoot:+http-not-found+) |
|
27 |
+ (values nil nil nil)))) |
|
28 |
+ (t (call-next-method)))) |