File: README.org

package info (click to toggle)
apiwrap-el 0.5-2
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 104 kB
  • sloc: lisp: 414; makefile: 2
file content (323 lines) | stat: -rw-r--r-- 13,341 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
#+Title: API-Wrap.el

=API-Wrap.el= is a tool to interface with the APIs of your favorite
services.  These macros make it easy to define efficient and
consistently-documented Elisp functions that use a natural syntax for
application development.

*** Lightning Tour
#+BEGIN_SRC elisp
  (require 'apiwrap)
  (require 'ghub)   ; backend for API primitives -- https://github.com/magit/ghub
  ;; typical backend is <150 lines

  (defun my-github-wrapper--request (method resource params data)
    (ghub-request (upcase (symbol-name method))
                  resource (apiwrap-plist->alist params) data))

  (apiwrap-new-backend "GitHub" "my-github-wrapper"
    '((repo . "REPO is a repository alist of the form returned by `/user/repos'."))
    :request #'my-github-wrapper--request)

  (defapiget-my-github-wrapper "/repos/:owner/:repo/issues"
    "List issues for a repository."
    "https://developer.github.com/v3/issues/#list-issues-for-a-repository"
    (repo) "/repos/:repo.owner.login/:repo.name/issues")

  ;; Check the docstring of this new function!
  (my-github-wrapper-get-repos-owner-repo-issues
   '((owner (login . "magit"))
     (name . "ghub")))
  ;; => parsed response of GET /repos/magit/ghub/issues
#+END_SRC

*** The API Primitive Request
=API-Wrap.el= can't do /all/ of the work for you; you will have to supply
your own primitive request function that can query the API and return
the processed response, but this is expected to be pretty /bare-bones/.
Without duplicating the documentation of ~apiwrap-new-backend~, the
macro expects a function like this:
#+BEGIN_SRC elisp
  (defun api-primitive (method resource params data)
    "Using METHOD, use RESOURCE with PARAMS and DATA.
  METHOD is a symbol -- one of `get', `put', `head', `post',
  `patch', or `delete'.

  RESOURCE is a string, like \"/users/20543/info\".

  PARAMS is a plist of parameters to RESOURCE.

  DATA is an alist of data for RESOURCE (e.g., for posting).")
#+END_SRC

If you have a function like this, you should be good to go!  If you
don't, you can likely model your primitive on =ghub='s (tiny) codebase.

It's not uncommon to need to write a wrapper!  =API-Wrap.el= is written
to be as unassuming as possible.  For instance, ~ghub-request~ does not
take the right kinds of parameters, but we can write a suitable
wrapper as above in the lightning tour.

* Defining API Backends
Suppose I want to use =ghub='s primitives to define a bunch of resource
wrappers to use in my application code.  To do this, I'll use
~apiwrap-new-backend~:
#+BEGIN_SRC elisp
  (require 'apiwrap)

  (eval-when-compile
    (apiwrap-new-backend "GitHub" "my-github-wrapper"
      '((repo . "REPO is a repository alist of the form returned by `/user/repos'.")
        (org  . "ORG is an organization alist of the form returned by `/user/orgs'."))
      :request #'my-github-wrapper--request
      :link (lambda (props)
              (format "https://developer.github.com/v3/%s"
                      (alist-get 'link props)))))
#+END_SRC
Refer to the macro's docstring for full a full description of the
parameters, but take away these highlights:
- we provide a service name that will be referenced in the
  documentation
- we provide a prefix for all macros and functions generated from
  here forward
- we provide an alist of standard parameters with standard
  documentation -- these parameters will be used by my application code
  and are usually similarly structured objects
- we provide a function to generate a link to the official API
  documentation from GitHub -- after all, these are just wrappers!
- we provide a primitive handler to handle making the request
- we provide a means to generate full documentation links based on
  subsequent macro arguments

Note that I must wrap this call (and all function definitions beyond
the primitives) in ~eval-when-compile~ so that =my-github-wrapper.el= will
compile.  If I don't ever need it to compile, I don't need this call.
When I do compile, though, I can rest easy knowing that the macros do
not make it into the byte-code -- only the actual, generated wrapper
functions do.  The macros are only generated once!

This /doesn't/ mean I don't have to ~(require 'apiwrap)~ every time,
though; application code will rely on support functions that evaluate
at runtime (like ~apiwrap-plist->alist~).

* Using the Generated Macros
After evaluating the above call to ~apiwrap-new-backend~, you will have six
new macros for your use:
- ~defapiget-my-github-wrapper~
- ~defapiput-my-github-wrapper~
- ~defapihead-my-github-wrapper~
- ~defapipost-my-github-wrapper~
- ~defapipatch-my-github-wrapper~
- ~defapidelete-my-github-wrapper~
These wonderful new macros super-charge your primitive API functions
into documentation-generating, resource-wrapping machines!  Let's
define a wrapper for the GitHub API endpoint =GET /issues=.

*** A simple use-case
Here is the definition of ~my-github-wrapper-get-issues~:
#+BEGIN_SRC elisp
  (defapiget-my-github-wrapper "/issues"
    "List all issues assigned to the authenticated user across all
  visible repositories including owned repositories, member
  repositories, and organization repositories."
    "issues/#list-issues")
#+END_SRC
If we refer to the documentation of ~defapiget-my-github-wrapper~, we'll
see that =/issues= is the method call as written in [[https://developer.github.com/v3/issues/#list-issues][the linked GitHub
API documentation]].  A brief docstring is provided, here copied from
the API.

If we now inspect the documentation of ~my-github-wrapper-get-issues~,
we'll see all of our information included in the docstring:
#+BEGIN_EXAMPLE
  my-github-wrapper-get-issues is a Lisp function.

  (my-github-wrapper-get-issues &optional DATA &rest PARAMS)

  List all issues assigned to the authenticated user across all
  visible repositories including owned repositories, member
  repositories, and organization repositories.

  DATA is a data structure to be sent with this request.  If it’s
  not required, it can simply be omitted.

  PARAMS is a plist of parameters appended to the method call.

  --------------------

  This generated function wraps the GitHub API endpoint

      GET /issues

  which is documented at

      URL ‘https://developer.github.com/v3/issues/#list-issues’
#+END_EXAMPLE
In addition to the documentation we provided, the =DATA= and =PARAMS=
parameters have been added to the function and appropriately
documented.  At the end of the documentaton, we report that the
function was generated from a raw method call and where that method is
fully documented (e.g., what =PARAMS= it accepts, what the format of
=DATA= is, the structure of its response, etc.).

*** On-the-fly parameters
Each function defined with the ~defapi*-my-github-wrapper~ macros
accepts =PARAMS= as a =&rest= argument.  This argument is effectively a
list of keyword arguments to the method call -- similar to how =&keys=
works in Common Lisp.  However, collecting them as a list allows us to
perform generic processing on them (with ~apiwrap-plist->alist~) so that
they can be passed straight to the request primitive.  For example,
#+BEGIN_SRC elisp
  ;; retrieve closed issues
  (my-github-wrapper-get-issues :state "closed")
#+END_SRC

If I wanted to use =:state 'closed= instead, I would need to handle that
in my primitive function (in this case, ~my-github-wrapper--request~).
For example, if I wanted to convert symbols to strings, I could modify
the function to say the following:
#+BEGIN_SRC elisp
  (defun my-github-wrapper--request (method resource params data)
    (ghub-request
     (upcase (sumbol-name method))
     resource
     (my-github-wrapper--preprocess-params params)
     data))

  (defun my-github-wrapper--preprocess-params (alist)
    (mapcar (lambda (cell)
              (if (symbolp (cdr cell))
                  (cons (car cell) (symbol-name (cdr cell)))
                cell))
            alist))
#+END_SRC

*** A complex use-case
Of course, many method calls accept 'interpolated' parameters
(so-called for lack of a better phrase).  Thanks to some very slick
macro-magic, ~defapi*-my-github-wrapper~ can handle these, too!

Consider this definition of
~my-github-wrapper-get-repos-owner-repo-issues~:
#+BEGIN_SRC elisp
  (defapiget-my-github-wrapper "/repos/:owner/:repo/issues"
    "List issues for a repository."
    "issues/#list-issues-for-a-repository"
    (repo) "/repos/:repo.owner.login/:repo.name/issues")
#+END_SRC
We've provided two extra parameters: =repo= and the string
=/repos/:repo.owner.login/:repo.name/issues=. Since
~defapiget-my-github-wrapper~ is a macro, =repo= is a just a symbol that
will be used in the argument list of the generated function (and
inserted into its docstring according to
~my-github-wrapper--standard-parameters~).

This second string is where things get interesting.  This argument
overrides the first, as-advertised method call for a very specific
purpose: when our new function is used, this string is evaluated in
the context of our =repo= object using syntax akin to ~let-alist~:
#+BEGIN_SRC elisp
  ;; repo "/repos/:repo.owner.login/:repo.name/issues"
  (my-github-wrapper-get-repos-owner-repo-issues
   '((owner (login . "vermiculus"))
     (name . "apiwrap.el")))
  ;; calls GET /repos/vermiculus/apiwrap.el/issues
#+END_SRC

*** Multiple required parameters
You may have noticed that you provided the =repo= symbol above in a
/list/.  You can have as many symbols as you want in this list; they
will all be evaluated in the string described above:
#+BEGIN_SRC elisp
  (defapiget-my-github-wrapper "/repos/:owner/:repo/issues/:number/comments"
    "List comments on an issue."
    "issues/comments/#list-comments-on-an-issue"
    (repo issue) "/repos/:repo.owner.login/:repo.name/issues/:issue.number/comments")
#+END_SRC
Each =:object= is considered for evaluation:
#+BEGIN_SRC elisp
  ;; repo issue "/repos/:repo.owner.login/:repo.name/issues/:issue.number/comments"
  (my-github-wrapper-get-repos-owner-repo-issues-number-comments
   '((owner (login . "vermiculus"))
     (name . "apiwrap.el"))
   '((number . 1)))
  ;; calls GET /repos/vermiculus/apiwrap.el/issues/1/comments
#+END_SRC

It's recommended that you treat each interpolated parameter as a full
object.  For example, I could've defined the above as
#+BEGIN_SRC elisp
  (defapiget-my-github-wrapper "/repos/:owner/:repo/issues/:number/comments"
    "List comments on an issue."
    "issues/comments/#list-comments-on-an-issue"
    (repo number) "/repos/:repo.owner.login/:repo.name/issues/:number/comments")
#+END_SRC
but I would not be able to pass an issue object into the function
without first getting its number out of the object.  If desired,
convenience functions can easily be written to create the sparse
object necessary to complete the API call:
#+BEGIN_SRC elisp
  (defun my-github-wrapper-issue-get-comments (repo issue-number)
    (my-github-wrapper-get-repos-owner-repo-issues-number-comments
     repo `((number . ,issue-number))))
#+END_SRC

*** Handling Errors
When writing a request primitive, it is usually wise to signal an
error for abnormal responses usually anything that's not =HTTP 2xx=.
Some API endpoints, however, may intentionally return an 'error'
status that in fact should not be considered an /error/ at all.  Take
for example the following GitHub API endpoint:
#+BEGIN_EXAMPLE
GET /repos/:owner/:repo
#+END_EXAMPLE
This returns the repository object if it exists and HTTP 404 if it
does not.  By convention, a wrapper function should probably return
=nil= instead to indicate there was nothing there.  This is where the
=:condition-case= keyword argument comes in:
#+BEGIN_SRC elisp
  (defapiget-my-github-wrapper "/repos/:owner/:repo"
    "Get a specific repository object."
    "repos/#get"
    (repo) "/repos/:repo.owner.login/:repo.name"
    :condition-case
    ((ghub-404 nil)))
#+END_SRC
Now, on receiving the =ghub-404= signal,
~my-github-wrapper-get-repos-owner-repo~ will return =nil= instead of
passing that error back up to the caller.

It's recommended that this configuration is not used at the
~apiwrap-new-backend~ level and instead left to each specific endpoint
per that endpoint's documentation.  Errors should still be errors --
think twice before you add any 'fancy' error-handling here.

** Other configuration
=API-Wrap.el= aims to be configurable enough to suit all kinds of needs.
Each call to ~defapi*-my-github-wrapper~ can take optional keyword
arguments as well.  These keyword arguments can override the default
values given in ~apiwrap-new-backend~.
* Using Macro-Generated Wrappers
This is the fun part!  The wrappers are a joy to use:

#+BEGIN_SRC elisp
  ;;; GET /issues
  (my-github-wrapper-get-issues)

  ;;; GET /issues?state=closed
  (my-github-wrapper-get-issues :state 'closed)

  (let ((repo (ghub-get "/repos/magit/magit")))
    (list
     ;; Magit's issues
     ;; GET /repos/magit/magit/issues
     (my-github-wrapper-get-repos-owner-repo-issues repo)

     ;; Magit's closed issues labeled 'easy'
     ;; GET /repos/magit/magit/issues?state=closed&labels=easy
     (my-github-wrapper-get-repos-owner-repo-issues repo
       :state 'closed :labels "easy")))
#+END_SRC
As an exercise, how would you wrap =(ghub-get "/repos/magit/magit")=?

I hope you enjoy using =API-Wrap.el= as much as I've enjoyed writing it!