git.fiddlerwoaroof.com
alimenta.lisp
9e44373f
 ;;;; alimenta.lisp -*- tab-width: 8; -*-
fb34c0f3
 ;; (declaim (optimize (speed 0) (safety 3) (debug 3)))
16cdb16d
 
 (in-package #:alimenta)
 
a16882ba
 (defclass feed-entity ()
5dc30073
   ((title :initarg :title :initform nil :accessor title)
a16882ba
    (link :initarg :link :initform nil :accessor link)
    (doc :initarg :doc :initform nil :accessor doc)))
 
 (defgeneric belongs-to (feed-entity)
   (:documentation "Returns the person responsible for this feed item"))
 
311bcb8e
 (define-condition feed-type-unsupported (error)
   ((%type :initarg :type :reader feed-type)
    (%feed-link :initarg :feed-link :reader feed-link)))
 
a4392760
 (defgeneric -to-feed (doc type &key feed-link)
311bcb8e
   (:documentation "Given an xml-document, return a feed object")
   (:method (doc type &key feed-link)
     (error 'feed-type-unsupported :type type :feed-link feed-link)))
a4392760
 
a16882ba
 (defgeneric render (object renderer)
   (:documentation "Given a lisp object representing a feed, return it rendered
                    to the specified format"))
 
a4392760
 (defgeneric generate-xml (feed feed-type &key partial)
   (:documentation "Given a lisp object representing a feed, return an xml
                    document"))
 
2815e4e1
 (defgeneric content-el (entity)
   (:documentation "Return the element that contains the item's content"))
 
a16882ba
 (defclass item (feed-entity)
a4392760
   ((author :initarg :author :initform nil :accessor author)
58a15352
    (content :initarg :content :initform nil :accessor content)
c5ce4bfd
    (description :initarg :description :accessor description)
a4392760
    (date :initarg :date :initform nil :accessor date)
    (id :initarg :id :initform nil :accessor id)
a16882ba
    (links :initform (make-hash-table :test #'equalp) :accessor links)))
 
5dc30073
 (collection-class:define-collection (feed item) (feed-entity)
a16882ba
   ((description :initarg :description :initform nil :accessor description)
    (feed-link :initarg :feed-link :initform nil :accessor feed-link)
    (source-type :initarg :source-type :initform nil :accessor source-type)))
 
 (defmethod render ((feed feed) renderer)
43ef15cf
   (let ((doc (alimenta.render:render-feed renderer feed)))
a16882ba
     (for:for ((item over feed))
2806d56a
       (setf doc
43ef15cf
             (alimenta.render:add-rendered-item renderer
                                                doc
                                                (alimenta.render:render-item renderer item feed))))
2806d56a
     doc))
a16882ba
 
 (defmethod (setf feed-link) ((value string) (feed feed))
   (setf (slot-value feed 'feed-link)
         (puri:parse-uri value)))
 
 (defmethod initialize-instance :after ((feed feed) &key feed-link)
   (when feed-link
     (setf (feed-link feed) (puri:parse-uri feed-link))))
 
 (defmethod belongs-to ((item item))
   (author item))
cd914c16
 
93a579ec
 (defclass complex-value () ())
 
 (defgeneric primary-value (self)
a4392760
   (:documentation "Primarily for COMPLEX-VALUES: this should take one and
                    return a useful primary value"))
16cdb16d
 
 (defgeneric push-item (feed item)
   (:documentation "Adds an item to the feed"))
 
cd914c16
 (defgeneric make-item (xml-dom doc-type)
   (:documentation "Given an xml document, return an item"))
16cdb16d
 
a4392760
 (defgeneric parse-feed (feed)
   (:documentation "Parse a feed into a lisp object"))
 
 (defgeneric get-items (xml feed-type)
   (:documentation "Given an xml document, extract its items"))
16cdb16d
 
a4392760
 (defmethod primary-value ((self t))
   self)
16cdb16d
 
a4392760
 (define-condition duplicate-link-type (error)
   ((old :reader duplicate-link-type-old :initarg :old)
    (new :reader duplicate-link-type-new :initarg :new))
   (:report (lambda (condition stream)
              (format stream "Item already has link ~s" (duplicate-link-type-old condition)))))
 
 (defmethod generate-xml :around ((feed feed) feed-type &rest r)
cd914c16
   (declare (ignore r))
   (let ((result (call-next-method feed feed-type)))
a16882ba
     (with-accessors ((items items)) feed
cd914c16
       (loop for item in items
fb34c0f3
          do (generate-xml item feed-type :partial result)))
cd914c16
     result))
 
2815e4e1
 (defmethod -to-feed ((doc stream) doc-type &key feed-link)
   (-to-feed (plump:parse doc)
fb34c0f3
             doc-type
             :feed-link feed-link))
2815e4e1
 
 (defmethod -to-feed ((doc string) doc-type &key feed-link)
   (-to-feed (plump:parse doc)
fb34c0f3
             doc-type
             :feed-link feed-link))
 
a4392760
 (defmethod -to-feed :around ((xml-dom plump:node) doc-type &key feed-link)
   "This wraps the particular methods so that _they_ don't have to implement
    item fetching.  NIL passed to the type activates auto-detection"
cd914c16
   (aprog1 (call-next-method xml-dom doc-type :feed-link feed-link)
     (with-slots (doc source-type) it
       (setf doc xml-dom
             source-type doc-type))
a16882ba
     (setf
fb34c0f3
      (items it)
      (loop for item across (get-items xml-dom doc-type)
         collect (make-item item doc-type)))))
cd914c16
 
 (defgeneric (setf link) (value self))
 (defmethod (setf link) ((value cons) (self item))
   (with-slots (links) self
     (destructuring-bind (type . href) value
       (when (consp href)
         (if (null (cdr href))
fb34c0f3
             (setf href (car href))
             (error 'type-error "too many arguments")))
cd914c16
       (let ((type-keyword (make-keyword (string-upcase type))))
         (when (slot-boundp self 'links)
5dc30073
           (multiple-value-bind (old-link old-link-p) (gethash type-keyword links)
cd914c16
             (when old-link-p
               (cerror "Replace Link ~a:~a with ~a:~a" 'duplicate-link-type :old old-link :new href))))
         (setf (gethash type-keyword links) href)))))
 
 (defmethod print-object ((object feed) stream)
   (print-unreadable-object (object stream :type t :identity t)
     (with-slots (title link) object
       (format stream "title: ~s link: ~s"
               (aif title (shorten-link it) "<untitled>")
               (aif link (shorten-link it) "<no link>")))))
 
 (defmethod print-object ((object item) stream)
   (print-unreadable-object (object stream :type t :identity t)
     (with-slots (title link date) object
       (format stream "title: ~s link: ~s date:~s"
               (aif title (shorten-link it) "<untitled>")
               (aif link (shorten-link it) "<no link>")
               (aif date it "<no date>")))))
 
 
 (defun detect-feed-type (xml-dom)
   (let ((root-node-name (make-keyword (string-upcase
fb34c0f3
                                        ($ (inline xml-dom) (children)
                                           (map #'tag-name) (node))))))
2815e4e1
     (case root-node-name
       ((:feed) :atom)
fa63b386
       ((:rdf :|RDF:RDF|) :rss)
2815e4e1
       (t root-node-name))))
cd914c16
 
a3645f2a
 (defgeneric get-random-item (feed)
   (:method ((feed feed))
     (let* ((items (copy-seq (items feed)))
            (num-items (length items)))
       (elt items
            (random num-items)))))
 
 (defgeneric get-latest-item (feed)
   (:method ((feed feed))
     (let ((items (copy-seq (items feed))))
       (car (sort items
                  #'local-time:timestamp>
                  :key #'date)))))
16cdb16d
 
2806d56a
 ;;(defun generate-xml (feed &key (feed-type :rss))
 ;;  (%generate-xml feed feed-type))
16cdb16d
 
 (defun to-feed (doc &key type feed-link)
a4392760
   "Makes an instance of feed from the given document.  Specialize to-feed with
16cdb16d
    an equal-specializer on type with an appropriate symbol to implement a new
    sort of feed."
   (unless type
     (setf type (detect-feed-type doc)))
a4392760
   (-to-feed doc type :feed-link feed-link))
 
 
2806d56a
 ;;(defun -get-items (feed xml-dom &key type)
 ;;  (with-accessors ((items items)) feed
 ;;    (loop for item across (get-items xml-dom type)
 ;;          do (push (make-item xml-dom type) items)
5dc30073
 ;;          finally (return items))))
16cdb16d
 
 (defun make-feed (&key title link items feed-link description)
a4392760
   (make-instance 'feed
                  :description description
                  :feed-link feed-link
                  :items items
                  :link link
                  :title title))
16cdb16d
 
 (let ((n 0))
   (defun next-id ()
     (incf n)))
 
 (defun add-item-to-feed (feed &key title (next-id #'next-id) date link content)
   (alet (make-instance 'item :title title :date date :link link :content content)
     (with-slots (id) it
       (setf id (funcall next-id it)))
     (push-item feed it)
     (values feed it)))
 
311bcb8e
 (defun filter-feed (feed function &key key)
   (setf (items feed)
fb34c0f3
         (remove-if-not function (items feed)
                        :key key))
311bcb8e
   feed)
 
fb34c0f3
 (defgeneric transform (item transform)
   (:documentation "transform a feed entity by TRANSFORM: the
 function will be called with either a feed or a item as an arguments
 and, if called upon a feed, it'll automatically be mapped across the
 feed's items after being called on the feed. We do not use the results
 of this mapping directly, however any modifications to an item mutate
 the original.")
 
   (:method :around (item transform)
2806d56a
            (call-next-method)
            item)
fb34c0f3
 
   (:method ((feed feed-entity) transform)
     (funcall transform feed))
 
   (:method :after ((feed feed) transform)
2806d56a
            (map nil (lambda (it)
                       (transform it transform))
                 (items feed))))
fb34c0f3
 
 (defun transform-content (item function)
   (setf (content item)
         (funcall function
                  (content item))))
 
cd914c16
 (defun shorten-link (link)
16cdb16d
   (let ((link (cl-ppcre:regex-replace "^https?:" link "")))
     (subseq link 0 (min 30 (length link)))))
 
 (defmethod push-item ((feed feed) (item item))
a16882ba
   (push item
         (items feed)))
16cdb16d
 
 (deftest push-item ()
2806d56a
   (let ((feed (make-instance 'feed))
         (item (make-instance 'item)))
     (with-accessors ((items items)) feed
       ;;(should signal error (push-item feed 2))
       (should be eql item
               (progn
                 (push-item feed item)
                 (car items))))))
16cdb16d
 
 ;; vim: set foldmethod=marker: