I wanted to share a nifty little Emacs Lisp macro I put together recently.
Previously, I'd been making REST API requests using Emacs's built-in `url-retrieve-synchronously` function, which generates a brand-new response buffer. I wrote a resource-managing macro for it that cleans up the buffer after setting point to the start of the response body and executing the macro body there, returning as its value the last expression in the body. For example:
(with-synchronous-download "https://api.github.com/.../jobs"
(loop for job across (json-read)
collect (job-summary job)))
It worked well enough, but in order to do things like send non-GET requests, include a request body or extra headers, etc, I had to bind global variables like `url-request-method`, `url-request-data`, etc. It just wasn't very readable, and there was little in the way of error handling. Eventually, I decided to try using request.el.
I found that request.el expects to do its work via side effect in a callback, but I wanted a synchronous parsed response. I eventually produced this macro wrapper (specialized for Github):
(defmacro github-request (path &rest body)
(let ((result (make-symbol "result")))
`(let (,result)
(request (format "https://api.github.com/repos/mycompany/%s" ,path)
:sync t
:headers (my-github-headers)
:parser (lambda () ,@body)
:success (lambda (&rest plist) (setq ,result (plist-get plist :data))))
,result)))
This worked fine for simple GET requests, but I was stumped at first how I might do POSTs or provide a request body. I couldn't just bind globals as I had before (and didn't want to), those would have to be additional keyword arguments passed to the `request` function. Somehow I needed to pass the keywords to the macro. I knew that I could use `cl-defmacro` to get `&key` parameters for it, but I also needed a `&rest` parameter for the body, and I knew from occasional bitter experience that those two parameter types don't mix as one might naively expect. Searching around for advice, I found a page somewhere that bluntly stated that keyword parsing had to be done manually, so that's what I did.
It took a few iterations to get a nice functional implementation, but this is what I ended up with:
(defmacro github-request (path &rest keys-and-body)
(letrec ((result (make-symbol "result"))
(parse (lambda (list)
(pcase list
(`(,(and (pred keywordp) key) ,value . ,rest)
`(,@(funcall parse rest) ,key ,value))
(body
`(:parser (lambda () ,@body)))))))
`(let (,result)
(request (format "https://api.github.com/repos/mycompany/%s" ,path)
:sync t
:headers (my-github-headers)
:success (lambda (&rest plist) (setq ,result (plist-get plist :data)))
,@(funcall parse keys-and-body))
,result)))
I discovered `letrec` while creating this macro, and decided to use it instead of a `let` to create the `result` symbol and a separate, nested `cl-labels` to define the recursive `parse` function, even at the expense of needing an extra `funcall` in two places.
Anyway, now I can do things like:
(github-request ".../jobs"
:method "POST"
:data '(("foo" . "bar"))
(loop for job across (json-read)
collect (job-summary job)))