Picking up from where we left off yesterday, let’s think about what we want our work-flow to be.
In terms of how the user uses
org-blog I want it to get out of the way as much as possible, so I’m going to try and keep the majority of the user’s interaction focused on two actions: starting a new post and saving it.
I have a couple of different blogs I post to, so I also want to make sure that
org-blog seamlessly supports managing content for more than one blog.
If we’re not going to have to type everything that describes a blog in each time we create or save a post, we have to have that configured somewhere. So right off the bat, we need to create a place to put our config info:
(defcustom org-blog-alist nil "An alist for specifying blog information. There are a number of parameters. Some day I will enumerate them.")
OK, the docstring is a little bit of a cop-out, but we don’t yet know what parameters will be pertinent. With a place to list the blogs we work with regularly, let’s look at creating a new post.
The first step will be to figure out what blog the user wants to write the post for. If there are no blogs configured, we can accept any name the user wants to give us. If there’s only one blog configured, we can reasonably assume that’s it. If there’s more than one, we should prompt with the available choices.
(defun org-blog-get-name (&optional post) "Get a name of a blog, perhaps working from a post. If we're given a post structure, we will extract the blog name from it. Otherwise, if there's only one entry in the `org-blog-alist', we will use that entry by default, but will accept anything, as long as the user confirms it, and if they don't enter anything at all, we default to unknown." (or (cdr (assoc :blog post)) (and (equal (length org-blog-alist) 1) (caar org-blog-alist)) (empty-string-is-nil (completing-read "Blog to post to: " (mapcar 'car org-blog-alist) nil 'confirm)) "unknown"))
You might be confused about the optional
post parameter. My crystal ball tells me that we will also want this function to be able to tell us what blog is associated with an already-existing post, so we have the option of passing in a
post structure that will be consulted for a
:blog entry, which we will prefer to anything else. Other than that the code is pretty much what I laid out above.
There is a reference to a small supplementary function that I was a little surprised I needed—it turns out that
completing-read will return the empty string if the user just hits enter. This doesn’t get us any useful information, so I wrote this short function to coerce the empty string to
nil, so the
or will fall through in that event:
(defun empty-string-is-nil (string) "Return any string except the empty string, which is coerced to nil." (unless (= 0 (length string)) string))
Now to write some tests. We want to test getting the blog name in all the ways that are available. For the last two tests, where we’re testing code paths that depend on the output of
completing-read, we take advantage of the
el-mock library to sub in a version that returns a constant that represents what we want to hear.
(ert-deftest ob-test-get-name-from-blog () "Test getting the blog name from a blog spec" (should (string= (org-blog-get-name '((:blog . "foo"))) "foo"))) (ert-deftest ob-test-get-name-from-alist () "Test getting the blog name from the alist" (let ((org-blog-alist '(("bar")))) (should (string= (org-blog-get-name) "bar")))) (ert-deftest ob-test-get-name-from-completing-read () "Test getting the blog name from completing-read" (with-mock (stub completing-read => "baz") (should (string= (org-blog-get-name) "baz")))) (ert-deftest ob-test-get-name-from-default () "Test getting the blog name from default" (with-mock (stub completing-read => "") (should (string= (org-blog-get-name) "unknown"))))