-
Notifications
You must be signed in to change notification settings - Fork 74
/
gh-api.el
258 lines (219 loc) · 9.98 KB
/
gh-api.el
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
;;; gh-api.el --- api definition for gh.el -*- lexical-binding: t; -*-
;; Copyright (C) 2011 Yann Hodique
;; Author: Yann Hodique <yann.hodique@gmail.com>
;; Keywords:
;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.
;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to
;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.
;;; Commentary:
;;
;;; Code:
(require 'eieio)
(require 'json)
(require 'gh-profile)
(require 'gh-url)
(require 'gh-auth)
(require 'gh-cache)
(require 'logito)
(defgroup gh-api nil
"Github API."
:group 'gh)
(defcustom gh-api-username-filter 'gh-api-enterprise-username-filter
"Filter to apply to usernames to build URL components"
:type 'function
:group 'gh-api)
(defclass gh-api ()
((sync :initarg :sync :initform t)
(cache :initarg :cache :initform nil)
(base :initarg :base :type string)
(profile :initarg :profile :type string)
(auth :initarg :auth :initform nil)
(data-format :initarg :data-format)
(num-retries :initarg :num-retries :initform 0)
(log :initarg :log :initform nil)
(cache-cls :initform gh-cache :allocation :class))
"Github API")
(cl-defmethod logito-log ((api gh-api) level tag string &rest objects)
(apply 'logito-log (oref api :log) level tag string objects))
(cl-defmethod initialize-instance ((api gh-api) &rest args)
(cl-call-next-method))
(cl-defmethod gh-api-set-default-auth ((api gh-api) auth)
(let ((auth (or (oref api :auth) auth))
(cache (oref api :cache))
(classname (symbol-name (funcall (if (fboundp 'eieio-object-class)
'eieio-object-class
'object-class)
api))))
(oset api :auth auth)
(unless (or (null cache)
(and (eieio-object-p cache)
(object-of-class-p cache 'gh-cache)))
(oset api :cache (make-instance
(oref api cache-cls)
:object-name
(format "gh/%s/%s"
classname
(gh-api-get-username api)))))))
(cl-defmethod gh-api-expand-resource ((api gh-api)
resource)
resource)
(defun gh-api-enterprise-username-filter (username)
(replace-regexp-in-string (regexp-quote ".") "-" username))
(cl-defmethod gh-api-get-username ((api gh-api))
(let ((username (oref (oref api :auth) :username)))
(funcall gh-api-username-filter username)))
(defclass gh-api-v3 (gh-api)
((data-format :initarg :data-format :initform :json))
"Github API v3")
(defcustom gh-api-v3-authenticator 'gh-oauth-authenticator
"Authenticator for Github API v3"
:type '(choice (const :tag "Password" gh-password-authenticator)
(const :tag "OAuth" gh-oauth-authenticator))
:group 'gh-api)
(cl-defmethod initialize-instance ((api gh-api-v3) &rest args)
(cl-call-next-method)
(let ((gh-profile-current-profile (gh-profile-current-profile)))
(oset api :profile (gh-profile-current-profile))
(oset api :base (gh-profile-url))
(gh-api-set-default-auth api
(or (oref api :auth)
(funcall gh-api-v3-authenticator "auth")))))
(defclass gh-api-request (gh-url-request)
((default-response-cls :allocation :class :initform gh-api-response)))
(defclass gh-api-response (gh-url-response)
())
(defun gh-api-json-decode (repr)
(if (or (null repr) (string= repr ""))
'empty
(let ((json-array-type 'list))
(json-read-from-string repr))))
(defun gh-api-json-encode (json)
(encode-coding-string (json-encode-list json) 'utf-8))
(cl-defmethod gh-url-response-set-data ((resp gh-api-response) data)
(cl-call-next-method resp (gh-api-json-decode data)))
(defclass gh-api-paged-request (gh-api-request)
((default-response-cls :allocation :class :initform gh-api-paged-response)
(page-limit :initarg :page-limit :initform -1)))
(defclass gh-api-paged-response (gh-api-response)
())
(cl-defmethod gh-api-paging-links ((resp gh-api-paged-response))
(let ((links-header (cdr (assoc "Link" (oref resp :headers)))))
(when links-header
(cl-loop for item in (split-string links-header ", ")
when (string-match "^<\\(.*\\)>; rel=\"\\(.*\\)\"" item)
collect (cons (match-string 2 item)
(match-string 1 item))))))
(cl-defmethod gh-url-response-set-data ((resp gh-api-paged-response) data)
(let ((previous-data (oref resp :data))
(next (cdr (assoc "next" (gh-api-paging-links resp)))))
(cl-call-next-method)
(oset resp :data (append previous-data (oref resp :data)))
(when (and next (not (equal 304 (oref resp :http-status))))
(let* ((req (oref resp :-req))
(last-page-limit (oref req :page-limit))
(this-page-limit (if (numberp last-page-limit) (- last-page-limit 1) -1)))
(oset req :page-limit this-page-limit)
(unless (eq (oref req :page-limit) 0)
;; We use an explicit check for 0 since -1 indicates that
;; paging should continue forever.
(oset resp :data-received nil)
(oset req :url next)
;; Params need to be set to nil because the next uri will
;; already have query params. If params are non-nil this will
;; cause another set of params to be added to the end of the
;; string which will override the params that are set in the
;; next link.
(oset req :query nil)
(gh-url-run-request req resp))))))
(cl-defmethod gh-api-authenticated-request
((api gh-api) transformer method resource &optional data params page-limit)
(let* ((fmt (oref api :data-format))
(headers (cond ((eq fmt :form)
'(("Content-Type" .
"application/x-www-form-urlencoded")))
((eq fmt :json)
'(("Content-Type" .
"application/json")))))
(cache (oref api :cache))
(key (list resource
method
(sha1 (format "%s" transformer))))
(cache-key (and cache
(member method (oref cache safe-methods))
key))
(has-value (and cache-key (pcache-has cache cache-key)))
(value (and has-value (pcache-get cache cache-key)))
(is-outdated (and has-value (gh-cache-outdated-p cache cache-key)))
(etag (and is-outdated (gh-cache-etag cache cache-key)))
(req
(and (or (not has-value)
is-outdated)
(gh-auth-modify-request
(oref api :auth)
;; TODO: use gh-api-paged-request only when needed
(make-instance 'gh-api-paged-request
:method method
:url (concat (oref api :base)
(gh-api-expand-resource
api resource))
:query params
:headers (if etag
(cons (cons "If-None-Match" etag)
headers)
headers)
:data (or (and (eq fmt :json)
(gh-api-json-encode data))
(and (eq fmt :form)
(gh-url-form-encode data))
"")
:page-limit page-limit)))))
(cond ((and has-value ;; got value from cache
(not is-outdated))
(make-instance 'gh-api-response :data-received t :data value))
(cache-key ;; no value, but cache exists and method is safe
(let ((resp (make-instance (oref req default-response-cls)
:transform transformer)))
(gh-url-run-request req resp)
(gh-url-add-response-callback
resp (make-instance 'gh-api-callback :cache cache :key cache-key
:revive etag))
resp))
(cache ;; unsafe method, cache exists
(pcache-invalidate cache key)
(gh-url-run-request req (make-instance
(oref req default-response-cls)
:transform transformer)))
(t ;; no cache involved
(gh-url-run-request req (make-instance
(oref req default-response-cls)
:transform transformer))))))
(defclass gh-api-callback (gh-url-callback)
((cache :initarg :cache)
(key :initarg :key)
(revive :initarg :revive)))
(cl-defmethod gh-url-callback-run ((cb gh-api-callback) resp)
(let ((cache (oref cb :cache))
(key (oref cb :key)))
(if (and (oref cb :revive) (equal (oref resp :http-status) 304))
(progn
(gh-cache-revive cache key)
(oset resp :data (pcache-get cache key)))
(pcache-put cache key (oref resp :data))
(gh-cache-set-etag cache key
(cdr (assoc "ETag" (oref resp :headers)))))))
(define-obsolete-function-alias 'gh-api-add-response-callback
'gh-url-add-response-callback "0.6.0")
(provide 'gh-api)
;;; gh-api.el ends here
;; Local Variables:
;; indent-tabs-mode: nil
;; End: