263 lines
9.9 KiB
EmacsLisp
263 lines
9.9 KiB
EmacsLisp
;;; gh-api.el --- api definition for gh.el
|
|
|
|
;; 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:
|
|
|
|
(eval-when-compile
|
|
(require 'cl))
|
|
|
|
;;;###autoload
|
|
(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)
|
|
|
|
;;;###autoload
|
|
(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")
|
|
|
|
(defmethod logito-log ((api gh-api) level tag string &rest objects)
|
|
(apply 'logito-log (oref api :log) level tag string objects))
|
|
|
|
(defmethod initialize-instance ((api gh-api) &rest args)
|
|
(call-next-method))
|
|
|
|
(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 (funcall (oref api cache-cls)
|
|
(format "gh/%s/%s"
|
|
classname
|
|
(gh-api-get-username api)))))))
|
|
|
|
(defmethod gh-api-expand-resource ((api gh-api)
|
|
resource)
|
|
resource)
|
|
|
|
(defun gh-api-enterprise-username-filter (username)
|
|
(replace-regexp-in-string (regexp-quote ".") "-" username))
|
|
|
|
(defmethod gh-api-get-username ((api gh-api))
|
|
(let ((username (oref (oref api :auth) :username)))
|
|
(funcall gh-api-username-filter username)))
|
|
|
|
;;;###autoload
|
|
(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)
|
|
|
|
(defmethod initialize-instance ((api gh-api-v3) &rest args)
|
|
(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)
|
|
(json-encode-list json))
|
|
|
|
(defmethod gh-url-response-set-data ((resp gh-api-response) data)
|
|
(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)
|
|
())
|
|
|
|
(defmethod gh-api-paging-links ((resp gh-api-paged-response))
|
|
(let ((links-header (cdr (assoc "Link" (oref resp :headers)))))
|
|
(when links-header
|
|
(loop for item in (split-string links-header ", ")
|
|
when (string-match "^<\\(.*\\)>; rel=\"\\(.*\\)\"" item)
|
|
collect (cons (match-string 2 item)
|
|
(match-string 1 item))))))
|
|
|
|
(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)))))
|
|
(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))))))
|
|
|
|
(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)))
|
|
|
|
(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:
|