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"))))