Browse code
Various updates: reworking the classes, etc.
fiddlerwoaroof authored on 07/03/2016 04:10:47
Showing 7 changed files
Showing 7 changed files
... | ... |
@@ -11,10 +11,13 @@ |
11 | 11 |
#:anaphora |
12 | 12 |
#:chronicity |
13 | 13 |
#:fwoar.lisputils |
14 |
+ #:split-sequence |
|
14 | 15 |
#:drakma) |
15 | 16 |
:serial t |
16 | 17 |
:components ((:file "package") |
17 | 18 |
(:file "alimenta") |
19 |
+ (:file "data-class") |
|
20 |
+ (:file "date-handling") |
|
18 | 21 |
(:file "atom") |
19 | 22 |
(:file "rss") |
20 | 23 |
(:file "fetching") |
... | ... |
@@ -6,7 +6,7 @@ |
6 | 6 |
(defclass feed () |
7 | 7 |
((title :initarg :title :initform nil) |
8 | 8 |
(link :initarg :link :initform nil) |
9 |
- (items :initarg :items :initform nil) |
|
9 |
+ (items :initarg :items :initform nil :accessor items) |
|
10 | 10 |
(description :initarg :description :initform nil) |
11 | 11 |
(feed-link :initarg :feed-link :initform nil) |
12 | 12 |
(doc :initarg :doc :initform nil) |
... | ... |
@@ -19,7 +19,7 @@ |
19 | 19 |
(date :initarg :date :initform nil) |
20 | 20 |
(link :initarg :link :initform nil) |
21 | 21 |
(links :initform (make-hash-table :test #'equalp)) |
22 |
- (content :initarg :content :initform nil) |
|
22 |
+ (content :initarg :content :initform nil :accessor content) |
|
23 | 23 |
(doc :initarg :doc :initform nil))) |
24 | 24 |
|
25 | 25 |
(defclass complex-value () ()) |
... | ... |
@@ -6,17 +6,11 @@ |
6 | 6 |
(label :initarg :label :initform nil) |
7 | 7 |
(scheme :initarg :scheme :initform nil))) |
8 | 8 |
|
9 |
-(defun make-category (term &optional label scheme) |
|
10 |
- (make-instance 'atom-category :term term :label label :scheme scheme)) |
|
11 |
- |
|
12 | 9 |
(defclass atom-person () |
13 | 10 |
((name :initarg :name :type (or null string) :initform nil) |
14 | 11 |
(uri :initarg :uri :type (or null string) :initform nil) |
15 | 12 |
(email :initarg :email :type (or null string) :initform nil))) |
16 | 13 |
|
17 |
-(defun make-person (name &optional uri email) |
|
18 |
- (make-instance 'atom-person :name name :uri uri :email email)) |
|
19 |
- |
|
20 | 14 |
(defclass atom-feed (alimenta:feed) |
21 | 15 |
((subtitle :initarg :subtitle :initform nil) |
22 | 16 |
(id :initarg :id :initform nil) |
... | ... |
@@ -26,16 +20,22 @@ |
26 | 20 |
(updated :initarg :updated :initform nil) |
27 | 21 |
(authors :initarg :authors :type (or null list) :initform nil))) |
28 | 22 |
|
23 |
+(defclass alimenta::link () |
|
24 |
+ ((alimenta::relation :initarg :rel) |
|
25 |
+ (alimenta::target :initarg :target))) |
|
26 |
+ |
|
29 | 27 |
(defclass atom-item (alimenta:item) |
30 | 28 |
((author-uri :initarg :author-uri :initform nil))) |
31 | 29 |
|
30 |
+(defun make-category (term &optional label scheme) |
|
31 |
+ (make-instance 'atom-category :term term :label label :scheme scheme)) |
|
32 |
+ |
|
33 |
+(defun make-person (name &optional uri email) |
|
34 |
+ (make-instance 'atom-person :name name :uri uri :email email)) |
|
35 |
+ |
|
32 | 36 |
(defmethod alimenta::%get-items (xml-dom (feed-type (eql :atom))) |
33 | 37 |
($ (inline xml-dom) "feed > entry")) |
34 | 38 |
|
35 |
-(defclass alimenta::link () |
|
36 |
- ((alimenta::relation :initarg :rel) |
|
37 |
- (alimenta::target :initarg :target))) |
|
38 |
- |
|
39 | 39 |
(defun get-link (xml) |
40 | 40 |
"This only handles alternate links" |
41 | 41 |
(let ((links ($ (inline xml) "> link[rel=alternate]" (combine (attr :type) (attr :href))))) |
... | ... |
@@ -28,20 +28,56 @@ |
28 | 28 |
`(setf ,name (when it (,transform it))) |
29 | 29 |
`(setf ,name it))))))) |
30 | 30 |
|
31 |
-(defgeneric all-slots (self format)) |
|
31 |
+(defgeneric %all-slots (self format)) |
|
32 |
+(defun all-slots (self &optional format) |
|
33 |
+ (%all-slots self format)) |
|
34 |
+ |
|
35 |
+(defgeneric slot-tags (self)) |
|
36 |
+ |
|
37 |
+(defun process-slots-for-data-class (slots) |
|
38 |
+ (mapcar |
|
39 |
+ (fw.lu::destructuring-lambda ((slot tag . rest)) |
|
40 |
+ (let ((tag (etypecase tag |
|
41 |
+ (symbol (string-downcase tag)) |
|
42 |
+ (string tag)))) |
|
43 |
+ (list* slot (make-keyword slot) tag rest))) |
|
44 |
+ (fw.lu:ensure-mapping slots))) |
|
45 |
+ |
|
46 |
+(deftest process-slots-for-data-class () |
|
47 |
+ (let ((tc-1 '(a)) |
|
48 |
+ (tc-2 '((a b))) |
|
49 |
+ (tc-3 '((a "b"))) |
|
50 |
+ (tc-4 '((a b c))) |
|
51 |
+ (tc-5 '((a "b" c))) |
|
52 |
+ (tc-6 '((a "bC"))) |
|
53 |
+ (tc-7 '((a "bC" d)))) |
|
54 |
+ (should be equal '((a :a "a")) (process-slots-for-data-class tc-1)) |
|
55 |
+ (should be equal '((a :a "b")) (process-slots-for-data-class tc-2)) |
|
56 |
+ (should be equal '((a :a "b")) (process-slots-for-data-class tc-3)) |
|
57 |
+ (should be equal '((a :a "b" c)) (process-slots-for-data-class tc-4)) |
|
58 |
+ (should be equal '((a :a "b" c)) (process-slots-for-data-class tc-5)) |
|
59 |
+ (should be equal '((a :a "bC")) (process-slots-for-data-class tc-6)) |
|
60 |
+ (should be equal '((a :a "bC" d)) (process-slots-for-data-class tc-7)))) |
|
32 | 61 |
|
33 | 62 |
(defmacro define-data-class (name (doc-slot root-el) (&rest superclasses) &body slots) |
34 |
- `(progn |
|
35 |
- (defclass ,name ,superclasses |
|
36 |
- ,(loop for (slot) in (fw.lu:ensure-mapping slots) |
|
37 |
- collect `(,slot :initarg ,(make-keyword slot) :accessor ,slot))) |
|
38 |
- ,@(loop for (slot tag-name . rest) in (fw.lu:ensure-mapping slots) |
|
39 |
- collect `(lazy-load-slot ,name ,doc-slot ,root-el ,slot ,tag-name ,@rest)) |
|
40 |
- (defmethod all-slots ((self ,name) format) |
|
41 |
- (pairlis (list ,@(mapcar (fw.lu::alambda (make-keyword (car it))) |
|
42 |
- (fw.lu:ensure-mapping slots))) |
|
43 |
- (list ,@(loop for (slot) in (fw.lu:ensure-mapping slots) |
|
44 |
- collect `(,slot self))))))) |
|
63 |
+ (declare (optimize (debug 3))) |
|
64 |
+ (flet ((make-slot-spec (slot slot-keyword) |
|
65 |
+ `(,slot :initarg ,slot-keyword :accessor ,slot))) |
|
66 |
+ (let ((slots (process-slots-for-data-class slots))) |
|
67 |
+ `(progn |
|
68 |
+ (defclass ,name ,superclasses |
|
69 |
+ ((slot-tags :allocation :class :initform |
|
70 |
+ ',(loop for (_ slot-keyword tag) in slots |
|
71 |
+ collect (cons slot-keyword tag))) |
|
72 |
+ ,@(mapcar (fw.lu::destructuring-lambda ((slot slot-keyword . r)) |
|
73 |
+ (declare (ignore r)) |
|
74 |
+ (make-slot-spec slot slot-keyword)) |
|
75 |
+ slots))) |
|
76 |
+ ,@(loop for (slot _ tag-name . rest) in slots |
|
77 |
+ collect `(lazy-load-slot ,name ,doc-slot ,root-el ,slot ,tag-name ,@rest)) |
|
78 |
+ (defmethod %all-slots ((self ,name) format) |
|
79 |
+ (pairlis (list ,@(mapcar (fw.lu::alambda (cadr it)) slots)) |
|
80 |
+ (list ,@(loop for (slot) in slots collect `(,slot self))))))))) |
|
45 | 81 |
|
46 | 82 |
|
47 | 83 |
|
... | ... |
@@ -1,11 +1,16 @@ |
1 | 1 |
(declaim (optimize (speed 0) (safety 3) (debug 3))) |
2 | 2 |
(in-package :alimenta.pull-feed) |
3 | 3 |
|
4 |
+(defmacro setup-libraries-for-feeds (&body body) |
|
5 |
+ `(let ((plump:*tag-dispatchers* plump:*xml-tags*) |
|
6 |
+ (drakma:*text-content-types* |
|
7 |
+ (pairlis '("application" "application") |
|
8 |
+ '("atom+xml" "rss+xml") |
|
9 |
+ drakma:*text-content-types*))) |
|
10 |
+ ,@body)) |
|
11 |
+ |
|
4 | 12 |
(defun fetch-doc-from-url (url) |
5 |
- (let ((plump:*tag-dispatchers* plump:*xml-tags*) |
|
6 |
- (drakma:*text-content-types* (concatenate 'list |
|
7 |
- '(("application" . "atom+xml") ("application" . "rss+xml")) |
|
8 |
- drakma:*text-content-types*))) |
|
13 |
+ (setup-libraries-for-feeds |
|
9 | 14 |
(plump:parse (drakma:http-request url)))) |
10 | 15 |
|
11 | 16 |
(define-condition fetch-error () ()) |
... | ... |
@@ -20,21 +25,27 @@ |
20 | 25 |
(cerror "Skip this feed" 'no-feed :url url)) |
21 | 26 |
|
22 | 27 |
(defun fetch-feed-from-url (url &key type) |
23 |
- (let* ((feeds (alimenta.discover:discover-feed (drakma:http-request url))) |
|
24 |
- (feeds (if type (remove-if-not (lambda (x) (eql type (car x))) feeds) feeds))) |
|
25 |
- (format t "~a << type" type) |
|
26 |
- (if (not feeds) (no-feed url) |
|
27 |
- (fetch-doc-from-url |
|
28 |
- (cdar |
|
29 |
- (restart-case |
|
30 |
- (if (cdr feeds) (feed-ambiguous feeds) feeds) |
|
31 |
- (take-first-feed nil |
|
32 |
- :report (lambda (s) (format s "Take the first feed")) |
|
33 |
- feeds) |
|
34 |
- (take-nth-feed (n) |
|
35 |
- :report (lambda (s) (format s "Take the nth feed")) |
|
36 |
- (list (elt feeds n))) |
|
37 |
- (select-feed (selector) |
|
38 |
- :report (lambda (s) (format s "Provide a function to select the right feed")) |
|
39 |
- (find-if selector feeds)))))))) |
|
28 |
+ (setup-libraries-for-feeds |
|
29 |
+ (let* ((feeds (alimenta.discover:discover-feed (drakma:http-request url))) |
|
30 |
+ (feeds (if type (remove-if-not (lambda (x) (eql type (car x))) feeds) feeds))) |
|
31 |
+ (if (not feeds) (no-feed url) |
|
32 |
+ (fetch-doc-from-url |
|
33 |
+ (cdar |
|
34 |
+ (restart-case |
|
35 |
+ (if (cdr feeds) (feed-ambiguous feeds) feeds) |
|
36 |
+ (take-first-feed nil |
|
37 |
+ :report (lambda (s) (format s "Take the first feed")) |
|
38 |
+ feeds) |
|
39 |
+ (take-nth-feed (n) |
|
40 |
+ :report (lambda (s) (format s "Take the nth feed")) |
|
41 |
+ (list (elt feeds n))) |
|
42 |
+ (select-feed (selector) |
|
43 |
+ :report (lambda (s) (format s "Provide a function to select the right feed")) |
|
44 |
+ (find-if selector feeds))))))))) |
|
40 | 45 |
|
46 |
+(defun pull-feed (url &key detect type) |
|
47 |
+ (to-feed |
|
48 |
+ (if detect |
|
49 |
+ (fetch-feed-from-url url) |
|
50 |
+ (fetch-doc-from-url url)) |
|
51 |
+ :type type)) |
... | ... |
@@ -6,16 +6,18 @@ |
6 | 6 |
|
7 | 7 |
(defpackage #:alimenta |
8 | 8 |
(:use #:cl #:should-test #:lquery #:plump #:alexandria #:anaphora) |
9 |
- (:export #:to-feed #:generate-xml |
|
10 |
- #:feed #:title #:link #:items #:feed-link #:doc #:source-type #:id #:date #:content |
|
11 |
- #:item #:description #:%generate-xml #:%to-feed #:%get-items #:make-item #:complex-value |
|
9 |
+ (:export #:to-feed #:generate-xml #:feed #:title #:link #:items #:feed-link |
|
10 |
+ #:doc #:source-type #:id #:date #:content #:item #:description |
|
11 |
+ #:%generate-xml #:%to-feed #:%get-items #:make-item #:complex-value |
|
12 | 12 |
#:primary-value)) |
13 | 13 |
|
14 | 14 |
(defpackage #:alimenta.rss |
15 |
- (:use #:cl #:should-test #:lquery #:plump #:alexandria #:anaphora #:alimenta) |
|
16 |
- (:export #:language #:copyright #:managing-editor #:webmaster #:publication-date #:last-build-date |
|
17 |
- #:categories #:generator #:docs #:cloud #:ttl #:image #:rating #:text-input #:skip-hours |
|
18 |
- #:skip-days #:rss-feed #:rss-item)) |
|
15 |
+ (:use #:cl #:should-test #:lquery #:plump #:alexandria #:anaphora #:alimenta #:data-class |
|
16 |
+ #:fwoar.lisputils) |
|
17 |
+ (:export #:language #:copyright #:managing-editor #:webmaster |
|
18 |
+ #:publication-date #:last-build-date #:categories #:generator #:docs |
|
19 |
+ #:cloud #:ttl #:image #:rating #:text-input #:skip-hours #:skip-days |
|
20 |
+ #:rss-feed #:rss-item)) |
|
19 | 21 |
|
20 | 22 |
(defpackage #:alimenta.atom |
21 | 23 |
(:use #:cl #:should-test #:lquery #:plump #:alexandria #:anaphora #:alimenta)) |
... | ... |
@@ -26,7 +28,8 @@ |
26 | 28 |
|
27 | 29 |
(defpackage #:alimenta.pull-feed |
28 | 30 |
(:use #:cl #:alimenta #:alexandria #:anaphora #:lquery) |
29 |
- (:export #:pull-feed #:fetch-doc-from-url)) |
|
31 |
+ (:export #:pull-feed #:fetch-doc-from-url #:fetch-feed-from-url |
|
32 |
+ #:fetch-error #:feed-ambiguous #:no-feed)) |
|
30 | 33 |
|
31 | 34 |
(defmethod asdf:perform ((o asdf:test-op) (s (eql (asdf:find-system :alimenta)))) |
32 | 35 |
(asdf:load-system :alimenta) |
... | ... |
@@ -1,20 +1,5 @@ |
1 | 1 |
(declaim (optimize (speed 0) (safety 3) (debug 3))) |
2 |
- |
|
3 | 2 |
(in-package :alimenta.rss) |
4 |
-(defun get-date (str) |
|
5 |
- (handler-case |
|
6 |
- (local-time:parse-timestring str) |
|
7 |
- (local-time::invalid-timestring (c) (declare (ignore c)) |
|
8 |
- (multiple-value-bind (res groups) (cl-ppcre:scan-to-strings "(.*)\s*([+-][0-9]{2,4})\s?$" str) |
|
9 |
- (let ((local-time:*default-timezone* local-time:+utc-zone+)) |
|
10 |
- (let* ((timestamp (string-trim " " (if res (elt groups 0) str))) |
|
11 |
- (hour-offset (if res (parse-integer (elt groups 1) :end 3) 0)) |
|
12 |
- (minute-offset (if (and res (> (length (elt groups 1)) 3)) |
|
13 |
- (* (signum hour-offset) (parse-integer (elt groups 1) :start 3)) |
|
14 |
- 0))) |
|
15 |
- |
|
16 |
- (local-time:timestamp- (local-time:timestamp- (chronicity:parse timestamp) minute-offset :minute) |
|
17 |
- hour-offset :hour))))))) |
|
18 | 3 |
|
19 | 4 |
|
20 | 5 |
(defclass rss-image () |
... | ... |
@@ -25,44 +10,10 @@ |
25 | 10 |
(height :initarg :height :initform nil) |
26 | 11 |
(description :initarg :description :initform nil))) |
27 | 12 |
|
28 |
-(defmethod print-object ((self rss-image) stream) |
|
29 |
- (print-unreadable-object (self stream :type t :identity t) |
|
30 |
- (format stream "~a" (slot-value self 'url)))) |
|
31 |
- |
|
32 |
-(defmethod primary-value ((self rss-image)) |
|
33 |
- (slot-value self 'url)) |
|
34 |
- |
|
35 |
-(defun make-image (url title &optional link width height description) |
|
36 |
- (let ((link (or link url))) |
|
37 |
- (make-instance 'rss-image |
|
38 |
- :url url |
|
39 |
- :title title |
|
40 |
- :link link |
|
41 |
- :width width |
|
42 |
- :height height |
|
43 |
- :description description))) |
|
44 |
- |
|
45 | 13 |
(defclass rss-category () |
46 | 14 |
((category :initarg :category :initform nil) |
47 | 15 |
(domain :initarg :domain :initform nil))) |
48 | 16 |
|
49 |
-(defmethod print-object ((self rss-category) stream) |
|
50 |
- (print-unreadable-object (self stream :type t :identity t) |
|
51 |
- (format stream "~a~@[ ~a~]" |
|
52 |
- (slot-value self 'category) |
|
53 |
- (slot-value self 'domain)))) |
|
54 |
- |
|
55 |
-(defmethod primary-value ((self rss-category)) |
|
56 |
- (slot-value self 'category)) |
|
57 |
- |
|
58 |
-(defun make-category (category &optional domain) |
|
59 |
- (make-instance 'rss-category :category category :domain domain)) |
|
60 |
- |
|
61 |
-(defun get-categories (doc tag) |
|
62 |
- ($ (inline doc) tag |
|
63 |
- (combine (text) (attr "domain")) |
|
64 |
- (map-apply #'make-category))) |
|
65 |
- |
|
66 | 17 |
(define-data-class rss-feed (doc "channel") (feed) |
67 | 18 |
language copyright webmaster |
68 | 19 |
generator docs cloud ttl rating |
... | ... |
@@ -87,6 +38,57 @@ |
87 | 38 |
(categories "category" :value (get-categories doc "> category")) |
88 | 39 |
source comments enclosure ) |
89 | 40 |
|
41 |
+(defmethod print-object ((self rss-image) stream) |
|
42 |
+ (print-unreadable-object (self stream :type t :identity t) |
|
43 |
+ (format stream "~a" (slot-value self 'url)))) |
|
44 |
+ |
|
45 |
+(defmethod print-object ((self rss-category) stream) |
|
46 |
+ (print-unreadable-object (self stream :type t :identity t) |
|
47 |
+ (format stream "~a~@[ ~a~]" |
|
48 |
+ (slot-value self 'category) |
|
49 |
+ (slot-value self 'domain)))) |
|
50 |
+ |
|
51 |
+(defun get-date (str) |
|
52 |
+ (handler-case |
|
53 |
+ (local-time:parse-timestring str) |
|
54 |
+ (local-time::invalid-timestring (c) (declare (ignore c)) |
|
55 |
+ (multiple-value-bind (res groups) (cl-ppcre:scan-to-strings "(.*)\s*([+-][0-9]{2,4})\s?$" str) |
|
56 |
+ (let ((local-time:*default-timezone* local-time:+utc-zone+)) |
|
57 |
+ (let* ((timestamp (string-trim " " (if res (elt groups 0) str))) |
|
58 |
+ (hour-offset (if res (parse-integer (elt groups 1) :end 3) 0)) |
|
59 |
+ (minute-offset (if (and res (> (length (elt groups 1)) 3)) |
|
60 |
+ (* (signum hour-offset) (parse-integer (elt groups 1) :start 3)) |
|
61 |
+ 0))) |
|
62 |
+ (let-each (:be *) |
|
63 |
+ (chronicity:parse timestamp) |
|
64 |
+ (local-time:timestamp- * minute-offset :minute) |
|
65 |
+ (local-time:timestamp- * hour-offset :hour)))))))) |
|
66 |
+ |
|
67 |
+ |
|
68 |
+(defmethod primary-value ((self rss-image)) |
|
69 |
+ (slot-value self 'url)) |
|
70 |
+ |
|
71 |
+(defun make-image (url title &optional link width height description) |
|
72 |
+ (let ((link (or link url))) |
|
73 |
+ (make-instance 'rss-image |
|
74 |
+ :url url |
|
75 |
+ :title title |
|
76 |
+ :link link |
|
77 |
+ :width width |
|
78 |
+ :height height |
|
79 |
+ :description description))) |
|
80 |
+ |
|
81 |
+(defmethod primary-value ((self rss-category)) |
|
82 |
+ (slot-value self 'category)) |
|
83 |
+ |
|
84 |
+(defun make-category (category &optional domain) |
|
85 |
+ (make-instance 'rss-category :category category :domain domain)) |
|
86 |
+ |
|
87 |
+(defun get-categories (doc tag) |
|
88 |
+ ($ (inline doc) tag |
|
89 |
+ (combine (text) (attr "domain")) |
|
90 |
+ (map-apply #'make-category))) |
|
91 |
+ |
|
90 | 92 |
(defmethod %get-items (xml-dom (feed-type (eql :rss))) |
91 | 93 |
($ (inline xml-dom) "channel > item")) |
92 | 94 |
|
... | ... |
@@ -102,10 +104,7 @@ |
102 | 104 |
(plump-dom:set-attribute |
103 | 105 |
($ (inline (make-element "guid")) (text id) (node)) |
104 | 106 |
"isPermaLink" |
105 |
- "false") |
|
106 |
- )) |
|
107 |
- |
|
108 |
- ))) |
|
107 |
+ "false")))))) |
|
109 | 108 |
|
110 | 109 |
(defmethod %generate-xml ((feed feed) (feed-type (eql :rss)) &rest r) |
111 | 110 |
(declare (ignore r)) |