git.fiddlerwoaroof.com
Ed Langley authored on 29/10/2020 23:58:30
Showing 6 changed files
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))))