Creating a relationship between a buffer and a post (part 2)

Last time, we looked at what was involved in extracting the information we wanted from our buffer and putting it into a post structure. Today we’ll look at the complimentary operation of merging the data from a post into a buffer.

This is a very important part of the process—there are certain things that we must record, like the id that the post is assigned by the blogging software, if we’re going to be able to really maintain the blog from within org-blog. While it would be very simple to skip this part, imagine if, once you had posted an entry for the first time, you had to log into your site in order to edit it? That would be a failing user experience. So, merge we must.

Really, though, the process is fairly straightforward. First, though, we want to establish a mapping between the property used within our post structure, and the property name (in the org-mode sense) that is used within the buffer:

(defconst mapping (list (cons :blog "POST_BLOG")
                        (cons :category "POST_CATEGORY")
                        (cons :date "DATE")
                        (cons :excerpt "DESCRIPTION")
                        (cons :id "POST_ID")
                        (cons :link "POST_LINK")
                        (cons :name "POST_NAME")
                        (cons :parent "POST_PARENT")
                        (cons :status "POST_STATUS")
                        (cons :tags "KEYWORDS")
                        (cons :title "TITLE")
                        (cons :type "POST_TYPE")))

Looking at this again with fresh eyes makes me realize that this data structure is going to get refactored before too long. As an example of why, let’s look back at a piece of yesterday’s code:

'(("POST_BLOG" :blog)
  ("POST_CATEGORY" :category)
  ("POST_ID" :id)
  ("POST_LINK" :link)
  ("POST_NAME" :name)
  ("POST_PARENT" :parent)
  ("POST_STATUS" :status)
  ("POST_TYPE" :type))

The fact that these two bits of structure are largely redundant should be pretty obvious. And I do actually have a plan for refactoring this (and a few other things) in a way that I think will clean up a lot of code. But I want to get the first version out before I worry about that too much—what I have is working, and I’d rather have people using it.

Anyway, that mapping structure is used in the function that actually update the buffer. The idea is pretty simple, really—pull out the current post values in the buffer, then iterate over the new values, making sure they’re formatted correctly, then examining the current value. If it’s nil, insert the new value at the head of the buffer, otherwise compare with the exiting value, and When they differ, find the pertinent property definition and update it.

To make sure that stuff goes in with a semblance of order, we sort a copy of the list before we start iterating—though, in truth, this doesn’t work as well as I’d like because we’re sorting on the property name, but inserting the property string, which may have “POST_” included. But that’s for another refactoring.

(defun org-blog-buffer-merge-post (merge)
  "Merge a post into a buffer.

Given a post structure (presumably returned from the server),
update the buffer to reflect the values it contains."
      ;; Get the current values
      (let ((current (org-blog-buffer-extract-post)))     (extract)
        (mapc                                             (iterate)
         (lambda (item)
           (let ((k (car item))
                 (v (cdr item))
                 val existing)
             (when (cdr (assq k mapping))
               (setq val (cond ((eq v nil)                (format)
                                (print "setting val to nil")
                               ((eq k :date)
                                (format-time-string "[%Y-%m-%d %a %H:%M]" (car v)))
                               ((listp v)
                                (mapconcat 'identity v ", "))
                               ((stringp v) 
               (goto-char (point-min))
               ;; (print (format "Comparison for %s is %s against %s" k v (cdr (assq k current))))
                ;; Inserting a new keyword
                ((eq (cdr (assq k current)) nil)          (new)
                 (when val
                   (insert (concat "#+" (cdr (assq k mapping)) ": " val "\n"))))
                ;; Updating an existing keyword
                ((not (equal (cdr (assq k current)) val)) (update)
                 (let ((re (org-make-options-regexp (list (cdr (assq k mapping))) nil))
                       (case-fold-search t))
                   (re-search-forward re nil t)
                   (replace-match (concat "#+" (cdr (assq k mapping)) ": " val) t t)))))))
         ;; Reverse sort fields to insert alphabetically
         (sort                                            (sort)
          (copy-alist merge)
          '(lambda (a b)
             (string< (car b) (car a)))))))))

When you come down to it, the process really is simple enough. The refactoring I envision is to create a table of all our post properties and the processes that need to be run to convert from post to buffer and back again, so that this routine becomes much more straightforward—map over each item, apply its formatter, see if it’s new and/or matches and behave appropriately. This could also be used in the extraction routine.

Anyway, I’m only going to show the last test, where we actually round-trip our structure. We create a temporary buffer, merge in a post structure, then extract a post from the resulting buffer, compare it against what we expect, then merge it back in a second time, and make sure that it matches again.

(ert-deftest ob-test-merge-round-trip ()
  "Try merging a full post into a full buffer, and make sure
you get the same thing out."
    (let ((post-string "#+POST_BLOG: t2b
#+POST_CATEGORY: t2c1, t2c2
#+DATE: [2013-01-25 Fri 00:00]
#+POST_ID: 1
#+POST_NAME: t2n
#+POST_STATUS: publish
#+KEYWORDS: t2k1, t2k2, t2k3
#+TITLE: Test 2 Title
#+POST_TYPE: post
          (post-struct '((:blog . "t2b")
                         (:category "t2c1" "t2c2")
                         (:content . "\n")
                         (:date (20738 4432))
                         (:excerpt . "t2e")
                         (:id . "1")
                         (:link . "")
                         (:name . "t2n")
                         (:parent . "0")
                         (:status . "publish")
                         (:tags "t2k1" "t2k2" "t2k3")
                         (:title . "Test 2 Title")
                         (:type . "post"))))
      (org-blog-buffer-merge-post post-struct)
      (should (equal (buffer-string) post-string))
      (should (equal (org-blog-buffer-extract-post) post-struct))
      (org-blog-buffer-merge-post post-struct)
      (should (equal (buffer-string) post-string)))))