Browse code
Finishing atom support, testing server.
- Added more complete support of the Atom suspect
- TODO: support dates !!!
- Added a test web-app
Showing 5 changed files
... | ... |
@@ -24,19 +24,11 @@ |
24 | 24 |
|
25 | 25 |
(defgeneric %to-feed (doc type &key feed-link)) |
26 | 26 |
|
27 |
-(defgeneric get-items (feed doc type) |
|
28 |
- (:documentation |
|
29 |
- "Get items for a feed. Specialize a symbol on type, to get items from a new sort of feed")) |
|
30 |
- |
|
31 | 27 |
(defgeneric %generate-xml (feed feed-type &key partial)) |
32 | 28 |
|
33 | 29 |
(defun generate-xml (feed &key (feed-type :rss)) |
34 | 30 |
(%generate-xml feed feed-type)) |
35 | 31 |
|
36 |
-(defmethod %generate-xml :around (feed feed-type &key partial) |
|
37 |
- (call-next-method feed feed-type :partial partial)) |
|
38 |
- |
|
39 |
- |
|
40 | 32 |
(defun to-feed (doc &key type feed-link) |
41 | 33 |
"Makes an instance of feed from the given document. Specialize %to-feed with |
42 | 34 |
an equal-specializer on type with an appropriate symbol to implement a new |
... | ... |
@@ -45,9 +37,12 @@ |
45 | 37 |
(setf type (detect-feed-type doc))) |
46 | 38 |
(%to-feed doc type :feed-link feed-link)) |
47 | 39 |
|
40 |
+(defgeneric %get-items (xml feed-type) |
|
41 |
+ (:method (xml-dom (feed-type (eql :rss))) ($ (inline xml-dom) "channel > item"))) |
|
42 |
+ |
|
48 | 43 |
(defun get-items (feed xml-dom &key type) |
49 | 44 |
(with-slots (items) feed |
50 |
- (loop for item across ($ (inline xml-dom) "channel > item") |
|
45 |
+ (loop for item across (%get-items xml-dom type) |
|
51 | 46 |
do (push (make-item xml-dom type) items) |
52 | 47 |
finally (return items)))) |
53 | 48 |
|
... | ... |
@@ -57,8 +52,11 @@ |
57 | 52 |
(aprog1 (call-next-method xml-dom doc-type :feed-link feed-link) |
58 | 53 |
(with-slots (doc source-type) it |
59 | 54 |
(setf doc xml-dom |
60 |
- source-type feed-link)) |
|
61 |
- (get-items it xml-dom :type doc-type))) |
|
55 |
+ source-type doc-type)) |
|
56 |
+ (with-slots (items) it |
|
57 |
+ (setf |
|
58 |
+ items (loop for item across (%get-items xml-dom doc-type) |
|
59 |
+ collect (make-item item doc-type)))))) |
|
62 | 60 |
|
63 | 61 |
(defgeneric feed-to-rss (feed)) |
64 | 62 |
(defgeneric feed-to-atom (feed)) |
... | ... |
@@ -126,7 +124,9 @@ |
126 | 124 |
(aif title (shorten-link it) "<untitled>") |
127 | 125 |
(aif link (shorten-link it) "<no link>"))))) |
128 | 126 |
|
129 |
-(defmethod %generate-xml ((feed feed) (feed-type (eql :rss)) &rest r) |
|
127 |
+(defclass rss-feed (feed) ()) |
|
128 |
+ |
|
129 |
+(defmethod %generate-xml ((feed rss-feed) (feed-type (eql :rss)) &rest r) |
|
130 | 130 |
(declare (ignore r)) |
131 | 131 |
(let* ((xml-root (plump:make-root)) |
132 | 132 |
(feed-root (plump:make-element xml-root "rss")) |
... | ... |
@@ -152,11 +152,35 @@ |
152 | 152 |
(defclass item () |
153 | 153 |
((title :initarg :title :initform nil) |
154 | 154 |
(id :initarg :id :initform nil) |
155 |
+ (author :initarg :author :initform nil) |
|
155 | 156 |
(date :initarg :date :initform nil) |
156 | 157 |
(link :initarg :link :initform nil) |
158 |
+ (links :initform (make-hash-table :test #'equalp)) |
|
157 | 159 |
(content :initarg :content :initform nil) |
158 | 160 |
(doc :initarg :doc :initform nil))) |
159 | 161 |
|
162 |
+(defgeneric (setf link) (value self)) |
|
163 |
+ |
|
164 |
+(define-condition duplicate-link-type (error) |
|
165 |
+ ((old :reader duplicate-link-type-old :initarg :old) |
|
166 |
+ (new :reader duplicate-link-type-new :initarg :new)) |
|
167 |
+ (:report (lambda (condition stream) |
|
168 |
+ (format stream "Item already has link ~s" (duplicate-link-type-old condition))))) |
|
169 |
+ |
|
170 |
+(defmethod (setf link) ((value cons) (self item)) |
|
171 |
+ (with-slots (links) self |
|
172 |
+ (destructuring-bind (type . href) value |
|
173 |
+ (when (consp href) |
|
174 |
+ (if (null (cdr href)) |
|
175 |
+ (setf href (car href)) |
|
176 |
+ (error 'type-error "too many arguments"))) |
|
177 |
+ (let ((type-keyword (make-keyword (string-upcase type)))) |
|
178 |
+ (when (slot-boundp self 'links) |
|
179 |
+ (multiple-value-bind (old-link old-link-p) (gethash type-keyword links) |
|
180 |
+ (when old-link-p |
|
181 |
+ (cerror "Replace Link ~a:~a with ~a:~a" 'duplicate-link-type :old old-link :new href)))) |
|
182 |
+ (setf (gethash type-keyword links) href))))) |
|
183 |
+ |
|
160 | 184 |
(defmethod %generate-xml ((item item) (feed-type (eql :rss)) &key partial) |
161 | 185 |
(prog1 partial |
162 | 186 |
(let ((item-root (make-element ($ (inline partial) "channel" (node)) "item"))) |
... | ... |
@@ -204,39 +228,12 @@ |
204 | 228 |
(doc-link ($ "channel > link" (text) (node))) |
205 | 229 |
(doc-feed-link (or feed-link |
206 | 230 |
($ "feed > atom::link[rel=self]" (first) (attr "href") (node))))) |
207 |
- (make-instance 'feed :title doc-title :link doc-link :feed-link doc-feed-link))) |
|
231 |
+ (make-instance 'rss-feed :title doc-title :link doc-link :feed-link doc-feed-link))) |
|
208 | 232 |
;}}} |
209 | 233 |
|
210 | 234 |
; {{{ ATOM feed handling |
211 |
-(defmethod make-item (xml-dom (type (eql :atom))) |
|
212 |
- (let* ((item-title ($ "> title" (text) (node))) |
|
213 |
- (item-link ($ "> link[rel=alternate]" (attr "href") (first) (node))) |
|
214 |
- (item-date (or ($ "> updated" (text) (node)) |
|
215 |
- ($ "> published" (text) (node)))) ;; Which should be default? |
|
216 |
- (item-guid ($ "> id" (text) (node))) |
|
217 |
- (item-description ($ "> summary" (text) (node))) |
|
218 |
- (item-content ($ "> content" (text) (node))) |
|
219 |
- (*tag-dispatchers* *html-tags*) |
|
220 |
- (content (with-output-to-string (s) |
|
221 |
- (serialize (parse (or item-content item-description)) s)))) |
|
222 |
- (make-instance 'item |
|
223 |
- :content content |
|
224 |
- :date item-date |
|
225 |
- :id item-guid |
|
226 |
- :link item-link |
|
227 |
- :title item-title))) |
|
228 | 235 |
|
229 |
-(defmethod %to-feed (xml-dom (type (eql :atom)) &key feed-link) |
|
230 |
- (declare (ignore type) (ignorable feed-link)) |
|
231 |
- ; TODO: store feed-link |
|
232 |
- (lquery:initialize xml-dom) |
|
233 |
- (let ((doc-title ($ "feed > title" (text) (node))) |
|
234 |
- (doc-link ($ "feed > link[rel=alternate]" (first) (attr "href") (node))) |
|
235 |
- (doc-feed-link (or feed-link |
|
236 |
- ($ "feed > link[rel=self]" (first) (attr "href") (node))))) |
|
237 |
- (make-instance 'feed :title doc-title :link doc-link :feed-link doc-feed-link))) |
|
238 |
-;}}} |
|
239 |
- |
|
236 |
+ |
|
240 | 237 |
(defun rdf-to-feed (xml-dom)) |
241 | 238 |
(defun json-to-feed (json-object)) |
242 | 239 |
(defun html5-to-feed (html-dom)) |
243 | 240 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,317 @@ |
1 |
+(declaim (optimize (debug 3) (safety 3) (speed 0))) |
|
2 |
+(in-package :alimenta.atom) |
|
3 |
+ |
|
4 |
+(defclass atom-category () |
|
5 |
+ ((term :initarg :term :initform nil) |
|
6 |
+ (label :initarg :label :initform nil) |
|
7 |
+ (scheme :initarg :scheme :initform nil))) |
|
8 |
+ |
|
9 |
+(defun make-category (term &optional label scheme) |
|
10 |
+ (make-instance 'atom-category :term term :label label :scheme scheme)) |
|
11 |
+ |
|
12 |
+(defclass atom-person () |
|
13 |
+ ((name :initarg :name :type (or null string) :initform nil) |
|
14 |
+ (uri :initarg :uri :type (or null string) :initform nil) |
|
15 |
+ (email :initarg :email :type (or null string) :initform nil))) |
|
16 |
+ |
|
17 |
+(defun make-person (name &optional uri email) |
|
18 |
+ (make-instance 'atom-person :name name :uri uri :email email)) |
|
19 |
+ |
|
20 |
+(defclass atom-feed (alimenta:feed) |
|
21 |
+ ((subtitle :initarg :subtitle :initform nil) |
|
22 |
+ (id :initarg :id :initform nil) |
|
23 |
+ (icon :initarg :icon :initform nil) |
|
24 |
+ (categories :initarg :categories :type (or null list) :initform nil) |
|
25 |
+ (logo :initarg :logo :initform nil) |
|
26 |
+ (authors :initarg :authors :type (or null list) :initform nil))) |
|
27 |
+ |
|
28 |
+(defclass atom-item (alimenta:item) |
|
29 |
+ ((author-uri :initarg :author-uri :initform nil))) |
|
30 |
+ |
|
31 |
+(defmethod alimenta::%get-items (xml-dom (feed-type (eql :atom))) |
|
32 |
+ ($ (inline xml-dom) "feed > entry")) |
|
33 |
+ |
|
34 |
+(defclass alimenta::link () |
|
35 |
+ ((alimenta::relation :initarg :rel) |
|
36 |
+ (alimenta::target :initarg :target))) |
|
37 |
+ |
|
38 |
+(defun get-link (xml) |
|
39 |
+ "This only handles alternate links" |
|
40 |
+ (let ((links ($ (inline xml) "> link[rel=alternate]" (combine (attr :type) (attr :href))))) |
|
41 |
+ (map 'list |
|
42 |
+ (lambda (x) |
|
43 |
+ (destructuring-bind (type href) x |
|
44 |
+ (setf (alimenta::link (make-keyword (string-upcase type))) |
|
45 |
+ (cons type href)))) |
|
46 |
+ links))) |
|
47 |
+ |
|
48 |
+(defmethod make-item (xml-dom (type (eql :atom))) |
|
49 |
+ (let* ((lquery:*lquery-master-document* xml-dom) |
|
50 |
+ (item-title ($ "> title" (text) (node))) |
|
51 |
+ (links ($ "> link" (combine (attr "rel") (attr "href")))) |
|
52 |
+ (sel-links (cadr (find-if (lambda (x) (aif (car x) (equal it "alternate") t)) |
|
53 |
+ links))) |
|
54 |
+ (item-link (or sel-links (cdr (when (> (length links) 0) (elt links 0))))) |
|
55 |
+ (item-date (or ($ "> updated" (text) (node)) |
|
56 |
+ ($ "> published" (text) (node)))) ;; Which should be default? |
|
57 |
+ (item-guid ($ "> id" (text) (node))) |
|
58 |
+ (item-description ($ "> summary" (text) (node))) |
|
59 |
+ (item-content ($ "> content" (text) (node))) |
|
60 |
+ (item-author ($ "> author > name" (text) (node))) |
|
61 |
+ (item-author-uri ($ "> author > uri" (text) (node))) |
|
62 |
+ (*tag-dispatchers* *html-tags*) |
|
63 |
+ (content (with-output-to-string (s) |
|
64 |
+ (awhen (or item-content item-description) (serialize (parse it) s))))) |
|
65 |
+ (make-instance 'atom-item |
|
66 |
+ :content content |
|
67 |
+ :date item-date |
|
68 |
+ :id item-guid |
|
69 |
+ :author item-author |
|
70 |
+ :author-uri item-author-uri |
|
71 |
+ :link item-link |
|
72 |
+ :title item-title))) |
|
73 |
+ |
|
74 |
+(defun get-authors (xml-dom) |
|
75 |
+ (let ((authors ($ (inline xml-dom) "feed > author"))) |
|
76 |
+ (loop for author across authors |
|
77 |
+ collect (make-person |
|
78 |
+ ($ (inline author) "> name" (text) (node)) |
|
79 |
+ ($ (inline author) "> uri" (text) (node)) |
|
80 |
+ ($ (inline author) "> email" (text) (node)))))) |
|
81 |
+ |
|
82 |
+(defmethod %to-feed (xml-dom (type (eql :atom)) &key feed-link) |
|
83 |
+ (declare (ignore type) (ignorable feed-link)) |
|
84 |
+ ; TODO: store feed-link |
|
85 |
+ (flet ((get-feed-elem (selector) ($ (inline xml-dom) selector (text) (node))) |
|
86 |
+ (get-feed-elem-attr (selector attr) ($ (inline xml-dom) selector (attr attr) (node)))) |
|
87 |
+ (let ((doc-title (get-feed-elem "feed > title")) |
|
88 |
+ (doc-subtitle (get-feed-elem "feed > subtitle")) |
|
89 |
+ (doc-summary (get-feed-elem "feed > summary")) |
|
90 |
+ (doc-icon (get-feed-elem "feed > icon")) |
|
91 |
+ (doc-logo (get-feed-elem "feed > logo")) |
|
92 |
+ (doc-id (get-feed-elem "feed > id")) |
|
93 |
+ (doc-link (get-feed-elem-attr "feed > link[rel=alternate]" "href")) |
|
94 |
+ (doc-feed-link (or feed-link (get-feed-elem-attr "feed > link[rel=self]" "href"))) |
|
95 |
+ (doc-categories ($ (inline xml-dom) "feed > category" |
|
96 |
+ (combine (attr "term") (attr "label") (attr "scheme")) |
|
97 |
+ (map-apply #'make-category))) |
|
98 |
+ (doc-authors (get-authors xml-dom))) |
|
99 |
+ (make-instance 'atom-feed |
|
100 |
+ :title doc-title |
|
101 |
+ :description doc-summary |
|
102 |
+ :icon doc-icon |
|
103 |
+ :logo doc-logo |
|
104 |
+ :link doc-link |
|
105 |
+ :id doc-id |
|
106 |
+ :feed-link doc-feed-link |
|
107 |
+ :subtitle doc-subtitle |
|
108 |
+ :categories (coerce doc-categories 'list) |
|
109 |
+ :authors doc-authors |
|
110 |
+ )))) |
|
111 |
+;}}} |
|
112 |
+ |
|
113 |
+(defmacro defconstants (&body constants) |
|
114 |
+ (list* |
|
115 |
+ 'progn |
|
116 |
+ (loop for (name value &optional doc) in constants |
|
117 |
+ collect `(defconstant ,name ,value ,doc)))) |
|
118 |
+ |
|
119 |
+(defvar *defconstants-really-verbose* nil) |
|
120 |
+(defmacro defconstants-really (&body constants) |
|
121 |
+ "auto-invoke the continue restart . . ." |
|
122 |
+ `(handler-bind ((sb-ext:defconstant-uneql |
|
123 |
+ (lambda (c) |
|
124 |
+ (when *defconstants-really-verbose* |
|
125 |
+ (format t "~&Changing definition of ~s from ~s to ~s~%" |
|
126 |
+ (sb-ext:defconstant-uneql-name c) |
|
127 |
+ (sb-ext:defconstant-uneql-old-value c) |
|
128 |
+ (sb-ext:defconstant-uneql-new-value c))) |
|
129 |
+ (continue c)))) |
|
130 |
+ (defconstants ,@constants))) |
|
131 |
+ |
|
132 |
+(defmethod %generate-xml ((feed feed) (feed-type (eql :atom)) &key partial) |
|
133 |
+ (let ((parent (or ($ (inline partial) "feed" (node)) |
|
134 |
+ (plump:make-element (plump:make-root) "feed")))) |
|
135 |
+ (prog1 parent |
|
136 |
+ (let ((feed-root (make-element parent "feed"))) |
|
137 |
+ (with-slots (title id link feed-link description) feed |
|
138 |
+ ($ (inline (make-element feed-root "title")) (text title) |
|
139 |
+ |
|
140 |
+ (inline (make-element feed-root "link")) |
|
141 |
+ (attr "href" feed-link) (attr "rel" "self") |
|
142 |
+ |
|
143 |
+ (inline (make-element feed-root "link")) |
|
144 |
+ (attr "href" link) (attr "rel" "alternate") (attr "type" "text/html") |
|
145 |
+ |
|
146 |
+ (inline (make-element feed-root "id")) (text id) (node) |
|
147 |
+ (inline (make-element feed-root "summary")) (text description) (node) |
|
148 |
+ )))))) |
|
149 |
+ |
|
150 |
+ |
|
151 |
+(defmethod %generate-xml ((item item) (feed-type (eql :atom)) &key partial) |
|
152 |
+ (let ((parent (or ($ (inline partial) "feed" (node)) |
|
153 |
+ (plump:make-element (plump:make-root) "feed")))) |
|
154 |
+ (prog1 parent |
|
155 |
+ (let ((item-root (make-element parent "entry"))) |
|
156 |
+ (with-slots (title id date link content (author alimenta::author) author-uri) item |
|
157 |
+ ($ (inline (make-element item-root "title")) (text title) |
|
158 |
+ (inline (make-element item-root "link")) (attr "href" link) |
|
159 |
+ (inline (make-element item-root "id")) (text id) (node) |
|
160 |
+ (inline (make-element item-root "pubDate")) (text date) |
|
161 |
+ (inline (make-element item-root "author")) |
|
162 |
+ (append ($ (inline (make-element item-root "name")) (text author))) |
|
163 |
+ (append ($ (inline (make-element item-root "uri")) (text author-uri))) |
|
164 |
+ (inline (make-element item-root "content")) (text content))))))) |
|
165 |
+ |
|
166 |
+ |
|
167 |
+(defconstants-really |
|
168 |
+ (+title+ "The Title") |
|
169 |
+ (+author+ "Joe Q Public") |
|
170 |
+ (+author-uri+ "http://example.com/joeq") |
|
171 |
+ (+content+ "Teh Content") |
|
172 |
+ (+id+ "t3_43tjwv") |
|
173 |
+ (+link+ "http://example.com/something") |
|
174 |
+ (+published+ "2016-02-02T09:41:27+00:00") |
|
175 |
+ |
|
176 |
+ (+entry1+ |
|
177 |
+ (format nil |
|
178 |
+ "<entry> |
|
179 |
+ <author> |
|
180 |
+ <name>~A</name> |
|
181 |
+ <uri>~A</uri> |
|
182 |
+ </author> |
|
183 |
+ <category term='programming' label='/r/programming'/> |
|
184 |
+ <content type='html'>~A</content> |
|
185 |
+ <id>~a</id> |
|
186 |
+ <link href='~a'/> |
|
187 |
+ <published>~a</published> |
|
188 |
+ <title>~a</title> |
|
189 |
+ </entry>" |
|
190 |
+ +author+ +author-uri+ +content+ +id+ +link+ |
|
191 |
+ +published+ +title+)) |
|
192 |
+ |
|
193 |
+ (+feed-category-term+ "testing") |
|
194 |
+ (+feed-category-label+ "/r/testing") |
|
195 |
+ (+feed-id+ "The Feed") |
|
196 |
+ (+feed-icon+ "http://example.com/feed.png") |
|
197 |
+ (+feed-logo+ "http://example.com/logo.png") |
|
198 |
+ (+feed-link-website+ "http://example.com") |
|
199 |
+ (+feed-link-self+ "http://example.com/atom.xml") |
|
200 |
+ (+feed-subtitle+ "The SubTitle") |
|
201 |
+ (+feed-title+ "The Title") |
|
202 |
+ (+feed-author-name+ "The Author") |
|
203 |
+ (+feed-author-uri+ "http://example.com/theauthor") |
|
204 |
+ (+feed-description+ "The description") |
|
205 |
+ (+feed1+ |
|
206 |
+ (format nil |
|
207 |
+ "<feed> |
|
208 |
+ <title>~a</title> |
|
209 |
+ <subtitle>~a</subtitle> |
|
210 |
+ <icon>~a</icon> |
|
211 |
+ <category term=\"~a\" label=\"~a\"/> |
|
212 |
+ <link rel=\"alternate\" href=\"~a\" type=\"text/html\" /> |
|
213 |
+ <link rel=\"self\" href=\"~a\" /> |
|
214 |
+ <logo>~a</logo> |
|
215 |
+ <summary>~a</summary> |
|
216 |
+ <author><name>~a</name><uri>~a</uri></author> |
|
217 |
+ <author><name>~a</name><uri>~a</uri></author> |
|
218 |
+ <id>~a</id> |
|
219 |
+ </feed>" |
|
220 |
+ +feed-title+ |
|
221 |
+ +feed-subtitle+ |
|
222 |
+ +feed-icon+ |
|
223 |
+ +feed-category-term+ +feed-category-label+ |
|
224 |
+ +feed-link-website+ |
|
225 |
+ +feed-link-self+ |
|
226 |
+ +feed-logo+ |
|
227 |
+ +feed-description+ |
|
228 |
+ +feed-author-name+ +feed-author-uri+ |
|
229 |
+ +feed-author-name+ +feed-author-uri+ |
|
230 |
+ +feed-id+ |
|
231 |
+ ))) |
|
232 |
+ |
|
233 |
+(defun true (x) (not (null x))) |
|
234 |
+ |
|
235 |
+(defun get-node-text (xml-doc selector) |
|
236 |
+ ($ (inline xml-doc) selector (text) (node))) |
|
237 |
+ |
|
238 |
+(deftest to-feed () |
|
239 |
+ (let ((xml (parse +feed1+))) |
|
240 |
+ (symbol-macrolet ((feed (alimenta::%to-feed xml :atom))) |
|
241 |
+ (should be equal +feed-title+ (slot-value feed 'alimenta:title)) |
|
242 |
+ (should be equal +feed-link-website+ (slot-value feed 'alimenta:link)) |
|
243 |
+ (should be equal +feed-link-self+ (slot-value feed 'alimenta:feed-link)) |
|
244 |
+ (should be equal +feed-description+ (slot-value feed 'description)) |
|
245 |
+ (should be equal +feed-id+ (slot-value feed 'id)) |
|
246 |
+ (should be equal +feed-subtitle+ (slot-value feed 'subtitle)) |
|
247 |
+ (should be equal +feed-icon+ (slot-value feed 'icon)) |
|
248 |
+ (should be equal +feed-logo+ (slot-value feed 'logo)) |
|
249 |
+ |
|
250 |
+ (should be equal +feed-category-term+ |
|
251 |
+ (slot-value |
|
252 |
+ (elt |
|
253 |
+ (slot-value feed 'categories) |
|
254 |
+ 0) |
|
255 |
+ 'term)) |
|
256 |
+ (should be equal +feed-category-label+ |
|
257 |
+ (slot-value |
|
258 |
+ (elt |
|
259 |
+ (slot-value feed 'categories) |
|
260 |
+ 0) |
|
261 |
+ 'label)) |
|
262 |
+ |
|
263 |
+ (should be equal +feed-author-name+ |
|
264 |
+ (slot-value |
|
265 |
+ (elt (slot-value feed 'authors) 0) |
|
266 |
+ 'name)) |
|
267 |
+ (should be equal +feed-author-uri+ |
|
268 |
+ (slot-value |
|
269 |
+ (elt (slot-value feed 'authors) 0) |
|
270 |
+ 'uri)) |
|
271 |
+ |
|
272 |
+ ;(should be equal +feed-title+ (slot-value feed 'alimenta:title)) |
|
273 |
+ ;(should be equal +feed-title+ (slot-value feed 'alimenta:title)) |
|
274 |
+ ;(should be equal +feed-title+ (slot-value feed 'alimenta:title)) |
|
275 |
+ ) |
|
276 |
+ ) |
|
277 |
+ ) |
|
278 |
+ |
|
279 |
+(deftest make-item () |
|
280 |
+ (let ((xml (lquery:$ (inline (plump:parse +entry1+)) "entry" (node)))) |
|
281 |
+ (symbol-macrolet ((item (alimenta::make-item xml :atom))) |
|
282 |
+ (should be true item) |
|
283 |
+ (should be equal +link+ (slot-value item 'alimenta:link)) |
|
284 |
+ (should be equal +content+ (slot-value item 'alimenta:content)) |
|
285 |
+ (should be equal +author+ (slot-value item 'alimenta::author)) |
|
286 |
+ (should be equal +author-uri+ (slot-value item 'author-uri)) |
|
287 |
+ (should be equal +id+ (slot-value item 'alimenta:id))))) |
|
288 |
+ |
|
289 |
+(defparameter *tmp* nil) |
|
290 |
+(deftest generate-xml () |
|
291 |
+ (let* ((xml ($ (inline (parse +entry1+)) "entry" (node))) |
|
292 |
+ (item (alimenta::make-item xml :atom))) |
|
293 |
+ (symbol-macrolet ((generated-xml (alimenta::%generate-xml item :atom))) |
|
294 |
+ (should be equal +title+ |
|
295 |
+ ($ (inline generated-xml) "entry > title" (text) (node))) |
|
296 |
+ (should be equal +author+ |
|
297 |
+ ($ (inline generated-xml) "entry > author > name" (text) (node))) |
|
298 |
+ (should be equal +author-uri+ |
|
299 |
+ ($ (inline generated-xml) "entry > author > uri" (text) (node))) |
|
300 |
+ (should be equal +id+ |
|
301 |
+ ($ (inline generated-xml) "entry > id" (text) (node))) |
|
302 |
+ (should be equal +content+ |
|
303 |
+ ($ (inline generated-xml) "entry > content" (text) (node))) |
|
304 |
+ (should be equal +link+ |
|
305 |
+ ($ (inline generated-xml) "entry > link" (attr "href") (node))) |
|
306 |
+ ; TODO: deal with dates . . . |
|
307 |
+ ))) |
|
308 |
+ |
|
309 |
+(defun do-test (&optional (test nil)) |
|
310 |
+ (let ((st:*test-output* *debug-io*)) |
|
311 |
+ (multiple-value-bind (result hm? errors) (st:test :test test) |
|
312 |
+ (format t |
|
313 |
+ "~&Returns: ~a~%Error:~%~{~a~^~%~}~%Failures-vals:~%~{~a~^ ~}~%" |
|
314 |
+ result |
|
315 |
+ errors |
|
316 |
+ hm? |
|
317 |
+ )))) |
... | ... |
@@ -4,7 +4,10 @@ |
4 | 4 |
(:use #:cl #:should-test #:lquery #:plump #:alexandria #:anaphora) |
5 | 5 |
(:export #:to-feed #:generate-xml |
6 | 6 |
#:feed #:title #:link #:items #:feed-link #:doc #:source-type #:id #:date #:content |
7 |
- #:item)) |
|
7 |
+ #:item #:description #:%generate-xml #:%to-feed #:%get-items #:make-item)) |
|
8 |
+ |
|
9 |
+(defpackage #:alimenta.atom |
|
10 |
+ (:use #:cl #:should-test #:lquery #:plump #:alexandria #:anaphora #:alimenta)) |
|
8 | 11 |
|
9 | 12 |
(defpackage #:alimenta.discover |
10 | 13 |
(:use #:cl #:alimenta #:alexandria #:anaphora #:lquery) |
... | ... |
@@ -18,3 +21,17 @@ |
18 | 21 |
(asdf:load-system :alimenta) |
19 | 22 |
(st:test :package :alimenta) |
20 | 23 |
t) |
24 |
+ |
|
25 |
+(defpackage #:alimenta.test-runner |
|
26 |
+ (:use #:cl #:alimenta #:alimenta.atom #:alimenta.discover #:alimenta.pull-feed)) |
|
27 |
+ |
|
28 |
+(in-package :alimenta.test-runner) |
|
29 |
+ |
|
30 |
+(defclass xunit-test (asdf:test-op) ()) |
|
31 |
+ |
|
32 |
+(defmethod asdf:perform ((o asdf:test-op) (s (eql (asdf:find-system :alimenta)))) |
|
33 |
+ (asdf:load-system :alimenta) |
|
34 |
+ (or (st:test :package :alimenta) |
|
35 |
+ (st:test :package :alimenta.atom) |
|
36 |
+ (st:test :package :alimenta.discover) |
|
37 |
+ (st:test :package :alimenta.pull-feed))) |
21 | 38 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,72 @@ |
1 |
+(ql:quickload :clack) |
|
2 |
+(ql:quickload :ningle ) |
|
3 |
+(ql:quickload :araneus) |
|
4 |
+(ql:quickload :spinneret) |
|
5 |
+(ql:quickload :lass) |
|
6 |
+ |
|
7 |
+(defmethod araneus:view ((name (eql 'root)) (item alimenta:item)) |
|
8 |
+ (with-slots ((title alimenta:title) (link alimenta:link)) item |
|
9 |
+ (spinneret:with-html |
|
10 |
+ (:article |
|
11 |
+ (:div.title title) |
|
12 |
+ (:a.link :href link link))))) |
|
13 |
+ |
|
14 |
+(defmethod araneus:view ((name (eql 'root)) (feed alimenta:feed)) |
|
15 |
+ (with-slots ((title alimenta:title) (link alimenta:link)) feed |
|
16 |
+ (spinneret:with-html |
|
17 |
+ (:header |
|
18 |
+ (:h1.feed-title title) |
|
19 |
+ (:a.feed-link link))))) |
|
20 |
+ |
|
21 |
+(defun get-css () |
|
22 |
+ (lass:compile-and-write |
|
23 |
+ `(* |
|
24 |
+ :padding "0px" |
|
25 |
+ :margin "0px") |
|
26 |
+ `(body |
|
27 |
+ :box-sizing "border-box" |
|
28 |
+ :font-family sans-serif |
|
29 |
+ ) |
|
30 |
+ |
|
31 |
+ `(header |
|
32 |
+ :border-bottom "thin solid black" |
|
33 |
+ :width "100%" |
|
34 |
+ :text-align center |
|
35 |
+ :margin-bottom "2em" |
|
36 |
+ :padding "1em") |
|
37 |
+ |
|
38 |
+ `(article |
|
39 |
+ :padding "1em" |
|
40 |
+ :border "4px double #888" |
|
41 |
+ :display "inline-block" |
|
42 |
+ :width "30%" |
|
43 |
+ :overflow "hidden" |
|
44 |
+ :min-height "4em" |
|
45 |
+ ) |
|
46 |
+ )) |
|
47 |
+ |
|
48 |
+(defmethod araneus:view :around ((name (eql 'root)) (feed alimenta:feed)) |
|
49 |
+ (with-slots ((title alimenta:title) (items alimenta::items)) feed |
|
50 |
+ (spinneret:with-html-string |
|
51 |
+ (:html |
|
52 |
+ (:head (:title title)) |
|
53 |
+ (:style |
|
54 |
+ :type "text/css" |
|
55 |
+ (get-css)) |
|
56 |
+ (:body |
|
57 |
+ (:main |
|
58 |
+ (call-next-method) |
|
59 |
+ (loop for item in items |
|
60 |
+ do (araneus:view 'root item)))))))) |
|
61 |
+ |
|
62 |
+(araneus:define-controller root (params) |
|
63 |
+ (let* ((url "https://reddit.com/r/programming.rss") |
|
64 |
+ (feed (alimenta.pull-feed::fetch-doc-from-url url))) |
|
65 |
+ (alimenta:to-feed feed :type :atom :feed-link url))) |
|
66 |
+ |
|
67 |
+(defvar *app* (make-instance 'ningle:<app>)) |
|
68 |
+ |
|
69 |
+(araneus:defroutes *app* |
|
70 |
+ (("/") (araneus:as-route 'root))) |
|
71 |
+ |
|
72 |
+(defvar *handler* (clack:clackup *app* :port 4939)) |