It’s probably a consequence of my recent study of functional programming—first with Haskell, and then, to a lesser extent, with Emacs Lisp itself—that I structured most of the important bits of org-blog as data transformations.
Really, one of the fundamental insights I gained while teaching myself Haskell was that it is extremely empowering to be able to know that when you call a function, nothing should be altered—that immutability really does increase your confidence that your program is doing what you think.
Now Emacs Lisp doesn’t have Haskell’s immutability or purity or any of those things—in fact, I have been a little dismayed to discover how many basic operations in Emacs Lisp mutate their arguments in one way or another—but it has most of the facilities you need to be able to program in that fashion.
So, with that in mind, the first step in actually posting things to a WordPress blog is going to be to get our
post structure into a format that we can feed to WordPress’s XML-RPC interface.
Step one in that process is to define a correspondence of some sort between a property in a
post structure and a name in a WordPress structure:
(defconst org-blog-wp-alist (list (cons :category "category") (cons :content "post_content") (cons :date "post_date_gmt") (cons :excerpt "post_excerpt") (cons :id "post_id") (cons :link "link") (cons :name "post_name") (cons :parent "post_parent") (cons :status "post_status") (cons :tags "post_tag") (cons :title "post_title") (cons :type "post_type")))
You will have probably realized by now that I go back and forth between quoting things like this and constructing them with
cons. I will go back and make things consistent eventually.
So this table is used by the actual transformation function, which looks like this:
(defun org-blog-post-to-wp (post) "Transform a post into a structure for submitting to WordPress. This is largely about mapping tag names, though the handling of `category' and `tags' is little more complex as the WordPress API now groups them as `taxonomies', and requires a hierarchical structure to differentiate them. For convenience in testing and inspection, the resulting alist is sorted." (sort (reduce '(lambda (wp new) (let ((k (car new)) (v (cdr new))) (when v (cond ((eq :category k) (setq wp (org-blog-post-to-wp-add-taxonomy wp "category" v))) ((eq :date k) ;; Convert to GMT by adding seconds offset (push (cons "post_date_gmt" (list :datetime (time-add (car v) (seconds-to-time (- (car (current-time-zone))))))) wp)) ((eq :tags k) (setq wp (org-blog-post-to-wp-add-taxonomy wp "post_tag" v))) ((eq :title k) (push (cons "post_title" (or v "No Title")) wp)) ((assq k org-blog-wp-alist) (push (cons (cdr (assq k org-blog-wp-alist)) v) wp)) ))) wp) post :initial-value nil) '(lambda (a b) (string< (car a) (car b)))))
Conceptually, this is simple—we’re running
reduce over the list of fields in the
post structure, and “accumulating” them into the
wp parameter we declare for our
The unfortunate complexity comes from the fact that while many fields can simply be copied over, a few require significant munging (
:date is the biggie, though we default our
:title as well—which should probably happen in the post-to-blog transformation now that I think on it), and the
:tags fields require a significant chunk of code to handle because instead of having separate fields for each in its XML-RPC interface, WordPress places the two fields under a higher-level structure called
taxonomies—and we don’t want to have an empty entry if a post is lacking either or both of the fields.
Thus we have the
(defun org-blog-post-to-wp-add-taxonomy (wp taxonomy entries) "Handle adding taxonomy items to a WordPress struct. The fiddly part is making sure that the sublists are sorted, for convenience in testing and inspection." (let* ((terms (assoc "terms_names" wp)) (existing (cdr terms)) (struct (cons taxonomy entries))) (if existing (progn (push struct existing) (setcdr terms (sort existing '(lambda (a b) (string< (car a) (car b)))))) (push (list "terms_names" struct) wp)) wp))
Simply put, if the
terms_names field already exists, we have to add our new “taxonomy” entry to it, but if it doesn’t exist, we need to create it. This is fiddlier than I would like it to be. I actually posted a question on StackOverflow to see if there was a cleaner way; the consensus was that although there were other strategies, there wasn’t anything a whole lot cleaner.
Now this is the place I put on my mea culpa hat, because as I re-examine these two functions, I see one thing I should be doing to make it cleaner—attaching the transformation functions for each field to their entries in the
org-blog-wp-alist. This would have several beneficial effects: the transformations would be closely associated with their related fields (thus easily kept up to date) and our
reduce becomes much less cluttered—just a function invocation per field. Also, instead of doing all those
setq invocations, I should let the result of
push (and other functions) simply be the result of the
cond, which becomes the result of the
lambda, which is the same as having
wp be the last
sexp in the lambda.
But before I make that change, I want to add a test to make sure that I don’t break anything:
(ert-deftest ob-test-posts-and-wp () "Transfer from buffers to posts and back again" (let ((post1-struct '((:blog . "t1b") (:category "t1c1" "t1c2") (:content . "\n<p>Test 1 Content\n</p>") (:date (20738 4432 0 0)) (:excerpt . "t1e") (:id . "1") (:link . "http://example.com/") (:name . "t1n") (:parent . "0") (:status . "publish") (:tags "t1k1" "t1k2" "t1k3") (:title . "Test 1 Title") (:type . "post"))) (post1-wp-input '(("link" . "http://example.com/") ("post_content" . "\n<p>Test 1 Content\n</p>") ("post_date_gmt" :datetime (20738 18832 0 0)) ("post_excerpt" . "t1e") ("post_id" . "1") ("post_name" . "t1n") ("post_parent" . "0") ("post_status" . "publish") ("post_title" . "Test 1 Title") ("post_type" . "post") ("terms_names" ("category" "t1c1" "t1c2") ("post_tag" "t1k1" "t1k2" "t1k3"))))) (should (equal (org-blog-post-to-wp post1-struct) post1-wp-input))))
And, with that done, in fact, I’m not going to make any changes right at the moment—I really want to figure out a sensible way to unify all of my
post-transformation tables in one place, which is a somewhat more ambitious change. So for the moment I’ll note the desire in a
FIXME comment, and move on. Tomorrow we’ll look at transforming from the structure WordPress outputs back into a post.