And now I will turn this hat into a rabbit

Before my digression about optimization and more idiomatic structures, we had just implemented the conversion of a post structure into a structure suitable for handing to WordPress. In a system like this, though, transformations always come in pairs, so we know that the complimentary WordPress to post structure operation has to be around here somewhere.

However, it’s important, I think, to realize that what WordPress returns to represent a post is a lot more than we put in. So here’s an example of what a WordPress post looks like, retrieved through XML-RPC and cast into sexps (automatically by the elisp xml-rpc module):

'(("post_id" . "1")
  ("post_title" . "Test 1 Title")
  ("post_date" :datetime
   (20738 4432))
  ("post_date_gmt" :datetime
   (20738 18832 0 0))
  ("post_modified" :datetime
   (20738 4432))
  ("post_modified_gmt" :datetime
   (20738 4432))
  ("post_status" . "publish")
  ("post_type" . "post")
  ("post_name" . "t1n")
  ("post_author" . "3075621")
  ("post_password")
  ("post_excerpt" . "t1e")
  ("post_content" . "\n<p>Test 1 Content\n</p>")
  ("post_parent" . "0")
  ("post_mime_type")
  ("link" . "http://example.com/")
  ("guid" . "http://example.com/")
  ("menu_order" . 0)
  ("comment_status" . "closed")
  ("ping_status" . "open")
  ("sticky")
  ("post_thumbnail")
  ("post_format" . "standard")
  ("terms"
   (("term_id" . "126039325")
    ("name" . "t1c1")
    ("slug" . "t1c1")
    ("term_group" . "0")
    ("term_taxonomy_id" . "4")
    ("taxonomy" . "category")
    ("description")
    ("parent" . "0")
    ("count" . 0))
   (("term_id" . "126039469")
    ("name" . "t1c2")
    ("slug" . "t1c2")
    ("term_group" . "0")
    ("term_taxonomy_id" . "5")
    ("taxonomy" . "category")
    ("description")
    ("parent" . "0")
    ("count" . 0))
   (("term_id" . "147991082")
    ("name" . "t1k1")
    ("slug" . "t1k1")
    ("term_group" . "0")
    ("term_taxonomy_id" . "6")
    ("taxonomy" . "post_tag")
    ("description")
    ("parent" . "0")
    ("count" . 0))
   (("term_id" . "147991085")
    ("name" . "t1k2")
    ("slug" . "t1k2")
    ("term_group" . "0")
    ("term_taxonomy_id" . "7")
    ("taxonomy" . "post_tag")
    ("description")
    ("parent" . "0")
    ("count" . 0))
   (("term_id" . "147991087")
    ("name" . "t1k3")
    ("slug" . "t1k3")
    ("term_group" . "0")
    ("term_taxonomy_id" . "8")
    ("taxonomy" . "post_tag")
    ("description")
    ("parent" . "0")
    ("count" . 0)))
  ("custom_fields"))

As you can see, there’s a lot of stuff in there that we don’t deal with in our post structure—the guid, the modification times, menu_order and more. Even more alarming is the sheer quantity of information we get back to describe categories and tags—they’re intermixed in the terms field, along with a lot of information we don’t intend to mess with.

We have a bit of a job condensing this stuff down.

I’m actually going to take a look at the bit of the code responsible for handling the terms field first. It takes the current post structure, as well as the list of terms entries, and updates the post structure to have appropriate :category and :tags fields, and is itself fairly straightforward:

(defun org-blog-wp-to-post-handle-taxonomy (post entries)
  "Handle mapping WordPress taxonomy info into a post struct.

We have to operate on all of the items in the taxonomy structure,
glomming them onto the existing post."
  (let* ((tlist (org-blog-wp-xml-terms-to-term-alist entries))
         (category (assoc "category" tlist))
         (tag (assoc "post_tag" tlist)))
    (when category
      (push (cons :category (cdr category)) post))
    (when tag
      (push (cons :tags (cdr tag)) post))))

(You might wonder if I should replace those calls to post with cons as I discussed yesterday. The answer is no: this function, unfortunately, exists for its side-effects in modifying post, so that’s not an option. Though I will probably rewrite it.)

All that’s doing, though, is adding a category or tag to our post when it’s present—the real action is in the function that takes the flat list from terms and turns it into an alist:

(defun org-blog-wp-xml-terms-to-term-alist (terms)
  "Handle turning WordPress taxonomy lists into an alist.

From here we can extract just the bits we need."
  (reduce
   '(lambda (lists term)
      (let ((name (cdr (assoc "name" term)))
            (taxonomy (cdr (assoc "taxonomy" term))))
        (cons (append (list taxonomy) (cdr (assoc taxonomy lists)) (list name)) lists)))
   terms :initial-value nil))

I had a much more convoluted version of this at one point, taking great care to remove the existing values for the attribute, because I lost sight of two complimentary attributes of lists in Lisp, and alists in particular.

The first is that cons takes the cons cell that is its first argument and sets its “next item” pointer (cdr) to point to the second argument. This is a constant-time operation, which is good, because you do it a lot in lisp, and it means whatever you cons goes to the front of the list.

The second is that when querying an alist, whether using assoc or assq or anything that looks at the first item, all the functions stop at the first mtach.

So instead of having to alter a list as I add terms to it, I can just cons the fully updated list onto the beginning of the results, and any time you search for that item in the alist, you will find the most up-to-date one first.

With all that term handling out of the way, the actual transformation function is kind of anticlimactic:

(defun org-blog-wp-to-post (wp)
  "Transform a WordPress struct into a post.

This is largely about mapping tag names, though the `terms'
structure benefits from a helper function to handle mapping it
properly.

For convenience in testing and inspection, the resulting alist is
sorted."
  (sort
   (reduce
    '(lambda (post new)
       "Do key and value transformations."
       (let ((k (car new))
             (v (cdr new)))
         (cond ((eq v nil)
                post)
               ((string= "terms" k)
                (org-blog-wp-to-post-handle-taxonomy post v))
               ((string= "post_date_gmt" k)
                ;; Must be a better way to extract this value
                (cons (cons (car (rassoc k org-blog-wp-alist)) (time-add (cadr v) (seconds-to-time (car (current-time-zone))))) post))
               ((rassoc k org-blog-wp-alist)
                (cons (cons (car (rassoc k org-blog-wp-alist)) v) post))
               (t
                 post))))
    wp :initial-value nil)
   '(lambda (a b)
      (string< (car a) (car b)))))

It’s just a variation on the existing transformation functions, doing the translation in a different direction. Which, again, argues for a more general implementation that’s using table-driven transformations for individual items, an idea I hope I’ll get to before too long.