git.fiddlerwoaroof.com
Raw Blame History
(in-package :alimenta.feed-archive.encoders)

(defun fix-path (path)
  ;; Work around some issue with pathnames
  (setf path (merge-pathnames path (make-pathname :type :unspecific))))

(defun skip-item ()
  (when-let ((restart (find-restart 'skip-item)))
    (invoke-restart restart)))

(define-condition feed-error (error)
  ((%feed :reader the-feed :initarg :feed :initform (error "We need a feed"))
   (%condition :reader the-condition :initarg :condition :initform (error "feed-error must wrap a condition"))))

(defun wrap-condition (condition feed)
  (error 'feed-error
         :feed feed
         :condition condition))

(defmacro unwrap-feed-errors (() &body body)
  `(handler-bind ((feed-error (op (error (the-condition _)))))
     ,@body))

(defun %encode-item (root-dir item)
  (destructuring-bind (title path) item
    (format t "~&Encoding ~a~%" title)
    (restart-once (fix-pathname () (fix-path path))
      (let ((pathname (uiop:enough-pathname path root-dir)))
        (yason:with-object ()
          (yason:encode-object-element "title" title)
          (yason:encode-object-element "path" pathname))))))

(defun %encode-feed-as-json (feed item-storage-info root-dir &optional stream)
  (with-accessors ((description alimenta:description)
                   (feed-link alimenta:feed-link)
                   (items alimenta:items)
                   (link alimenta:link)
                   (source-type alimenta:source-type)
                   (title alimenta:title)) feed
    (yason:with-output (stream :indent t)
      (yason:with-object ()
        (yason:encode-object-element "metadata" feed)
        (yason:with-object-element ("items")
          (yason:with-array ()
            (dolist (item item-storage-info)
              (with-simple-restart (skip-item "Skip item ~s" (car item))
                ;; (format t "~&I Store Info: ~a~%~4t~a~%" (uiop:unix-namestring (cadr item)) root-dir)
                (%encode-item root-dir item)
                #+null
                (yason:encode-array-element (uiop:unix-namestring (uiop:enough-pathname root-dir (cadr item))))
                ))))))))

(defmethod store ((items sequence) storage)
  (when (next-method-p)
    (format t "calling next...~%")
    (call-next-method))
  (map 'list (op (store _ storage))
       (stable-sort (sort items #'string-lessp
                          :key #'alimenta:title)
                    #'local-time:timestamp>
                    :key #'alimenta:date)))

(defmethod store ((feed alimenta:feed) (directory pathname))
  (flet ((get-feed-store-name (feed directory)
           (merge-pathnames (get-id feed)
                            directory)))

    (with-accessors ((description alimenta:description)
                     (feed-link alimenta:feed-link)
                     (items alimenta:items)
                     (link alimenta:link)
                     (source-type alimenta:source-type)
                     (title alimenta:title)) feed
                                        ; We wrap all errors with our own condition
      (handler-bind ((error (lambda (c) (error 'feed-error :feed feed :condition c))))
        (values (multiple-value-list
                 (prog1-let ((feed-title title)
                             (feed-store (get-feed-store-name feed directory)))
                   (ensure-directories-exist feed-store)
                   (with-open-file (index (merge-pathnames "index.json" feed-store) :direction :output)
                     (%encode-feed-as-json feed
                                           (store (copy-seq items) feed-store)
                                           feed-store
                                           index))))
                feed-link)))))

(defmethod store ((feed alimenta:feed) (stream stream))
  (handler-bind ((error (lambda (c)
                          (typecase c
                            (feed-error c)
                            (t (wrap-condition c feed))))))
    (yason:with-output (stream :indent t)
      (yason:with-object ()
        (yason:with-object-element ("metadata")
          (yason:encode-object feed))
        (yason:with-object-element ("items")
          (yason:with-array ()
            (for:for ((item over feed))
              (store item stream))))))
    (list (alimenta:title feed)
          stream)))

(defmethod store ((item alimenta:item) (directory pathname))
  (flet ((get-item-store-name (item directory)
           (let ((id (get-id item)))
             (merge-pathnames (make-pathname :name id :version nil :type "json") directory))))

    (multiple-value-list
     (prog1-let ((item-title (alimenta:title item))
                 (fn (get-item-store-name item directory)))
       (with-open-file (item-f fn :direction :output)
         (yason:encode item item-f))))))

(defmethod store ((item alimenta:item) (stream stream))
  (yason:with-output (stream :indent t)
    (yason:with-object ()
      (yason:encode-slots item)))
  (list (alimenta:title item)
        stream))

;; The feed is always index.json
(defmethod stream-provider:stream-key (provider (feed alimenta:feed))
  (pathname
   (string-join
    (list (get-id feed)
          "index.json")
    "/")))

(defmethod stream-provider:stream-key :around ((provider stream-provider:file-provider)
                                               (feed alimenta:feed))
  (prog1-bind (result (call-next-method))
    (ensure-directories-exist (merge-pathnames result
                                               (stream-provider:root provider)))))

(defmethod stream-provider:stream-key (provider (item alimenta:item))
  (let ((id (get-id item)))
    (make-pathname :name id :version nil :type "json")))

(defclass feed-stream-item-provider ()
  ((%item-providers :accessor item-providers :initform (make-hash-table :test 'equal))))

(defclass feed-stream-provider (stream-provider:file-provider feed-stream-item-provider)
  ())

(defclass feed-stream-string-provider (stream-provider:string-provider feed-stream-item-provider)
  ())

(defmethod stream-provider:get-nested-provider ((provider stream-provider:stream-provider) (streamable alimenta:feed))
  (with (items-root (uiop:merge-pathnames* (uiop:pathname-directory-pathname (stream-provider:stream-key provider streamable))
                                           (stream-provider:root provider)))
    (ensure-gethash items-root
                    (item-providers provider)
                    (make-instance 'stream-provider:file-provider :root items-root))))

(defmethod store :around ((item alimenta:feed-entity) (stream-provider stream-provider:stream-provider))
  (call-next-method)
  (list (alimenta:title item)
        (stream-provider:stream-key stream-provider item)))

(defmethod store ((item alimenta:item) (stream-provider stream-provider:stream-provider))
  (stream-provider:with-storage-stream (s item stream-provider)
    (store item s)))

(defmethod store :around ((item alimenta:item) (dest stream-provider:stream-provider))
  (with-simple-restart (skip-item "Skip item ~s" (car item))
    (call-next-method)))

(defun map-coalesce (fun &rest seqs)
  (apply #'mappend
         (compose #'unsplice
                  fun)
         seqs))

(defmethod store ((feed alimenta:feed) (stream-provider stream-provider:stream-provider))
  (stream-provider:with-storage-stream (s feed stream-provider)
    (with-accessors ((description alimenta:description)
                     (feed-link alimenta:feed-link)
                     (items alimenta:items)
                     (link alimenta:link)
                     (source-type alimenta:source-type)
                     (title alimenta:title)) feed
      (let* ((item-provider (stream-provider:get-nested-provider stream-provider feed))
             (item-storage-info (map-coalesce (op (store _ item-provider))
                                              items)))
        (let ((yason::*json-output*
                (make-instance 'yason::json-output-stream
                               :output-stream s
                               :indent t)))
          (with-collection (item "items" item-storage-info "metadata" feed)
            (destructuring-bind (title path) item
              (yason:with-object ()
                (yason:encode-object-elements "title" title "path" path)))))))))