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.