git.fiddlerwoaroof.com
Raw Blame History
;;;; jira-api.lisp

(in-package #:jira-api)

;;; "jira-api" goes here. Hacks and glory await!

; curl -u user:password https://atomampd.atlassian.net/rest/api/2/issue/ATOMOS-212 | jq .
(defparameter *endpoint* "https://socraticum.atlassian.net/rest/api/2/")

(defun api-get-call (auth method &rest parameters)
  "Connect to a GET REST endpoint specified by method and return a stream from
   which the response can be read."
  (let ((drakma:*text-content-types* (acons "application" "json" drakma:*text-content-types*)))
    (drakma:http-request (puri:merge-uris method *endpoint*)
                         :parameters (alexandria:plist-alist parameters)
                         :basic-authorization auth
                         :want-stream t)))

(defun api-post-call (auth method post-data)
  "Connect to a GET REST endpoint specified by method and return a stream from
   which the response can be read."
  (let ((drakma:*text-content-types* (acons "application" "json" drakma:*text-content-types*)))
    (drakma:http-request (puri:merge-uris method *endpoint*)
                         :method :POST
                         :content-type "application/json"
                         :content post-data
                         :basic-authorization auth
                         :want-stream t)))

(defun get-issues (auth)
  (api-get-call auth "search"))

(defun get-issue (auth key &rest params &key expand)
  (declare (ignore expand))
  (apply 'api-get-call auth (format nil "issue/~a" key) params))

(defun get-projects (auth)
  (api-get-call auth "issue/createmeta"))

(defun make-issue (project-key summary &key (description nil description-p) (issue-type "Bug"))
  (let* ((issue (make-hash-table :test 'equal))
         (project (alist-string-hash-table `(("key" . ,project-key))))
         (issue-type (alist-string-hash-table `(("name" . ,issue-type)))))
    (setf (gethash "fields" issue)
          (alist-string-hash-table
            `(("project" . ,project)
              ("summary" . ,summary)
              ,@(when description-p
                  (list
                    (cons "description" description)))
              ("issuetype" . ,issue-type))))
    issue))

(defun post-issue (auth issue)
  (let-each (:be *)
    issue
    (with-output-to-string (s)
      (yason:encode * s))
    (api-post-call auth "issue" *)))

(defun post-issue-from-fields (auth project-key summary &key
                        (description nil description-p)
                        (issue-type nil issue-type-p))
  (let ((optional-arguments '()))
    (when issue-type-p
      (push issue-type optional-arguments))
    (when description-p
      (push description optional-arguments))

    (let-each (:be *)
      (apply #'make-issue project-key summary optional-arguments)
      (post-issue auth *))))

(defun read-issue (&optional (iostream *query-io*))
  (let ((project-key (prompt :project-key iostream))
        (summary (prompt :summary iostream))
        (description (optional-prompt iostream :description "Add a description? ")))
    (apply #'make-issue project-key summary
           (list*
             (when description
               (list description))))))

(deftype vector-of-objects () '(vector (or hash-table sheeple:object) *))

(defun json2sheeple (json)
  (labels
    ((handle-parsed (parsed-json)
       (typecase parsed-json
         (vector-of-objects (map 'vector #'handle-parsed parsed-json))
         (hash-table
           (let ((result (sheeple:object)))
             (loop for json-prop being the hash-keys of parsed-json using (hash-value json-value)
                   do (setf (sheeple:property-value result
                                                    (intern (string-upcase json-prop) :jira-api))
                            (typecase json-value
                              (hash-table (handle-parsed json-value))
                              (vector-of-objects (map 'vector #'handle-parsed json-value))
                              (t json-value)))
                   finally (return result))))
         (t parsed-json))))
    (let ((yason:*parse-json-arrays-as-vectors* t))
      (handle-parsed (yason:parse json)))))

(defun show-person (person title)
  (sheeple:with-properties (displayname emailaddress) person
    (format t "~&~4t~a: \"~a\" <~a>~%" title displayname emailaddress)))

(defun show-labels (labels)
  (format t "~&~4tLabels: ~{~a~}~%" (coerce labels 'list)))

(defun show-description (description)
  (pprint-logical-block (*standard-output* (mapcar (compose 'tokens 'trim-whitespace)
                                                 (lines description)))
    (pprint-indent :block 4 *standard-output*)
    (pprint-newline :mandatory *standard-output*)
    (loop
      (pprint-exit-if-list-exhausted)
      (let ((line (pprint-pop)))
        (pprint-logical-block (*standard-output* line)
          (loop
            (princ (pprint-pop) *standard-output*)
            (pprint-exit-if-list-exhausted)
            (princ #\space *standard-output*)
            (pprint-indent :block 3)
            (pprint-newline :fill *standard-output*)))
        (pprint-newline :mandatory *standard-output*)))))

(defun show-summary (summary)
  (pprint-logical-block (*standard-output* (funcall (compose 'tokens 'trim-whitespace) summary))
    (pprint-indent :block 8 *standard-output*)
    (pprint-exit-if-list-exhausted)
    (format *standard-output* "~4tSummary: ")
    (loop
      (princ (pprint-pop))
      (pprint-exit-if-list-exhausted)
      (pprint-newline :fill *standard-output*)
      (princ #\space))))

(defun classify-issues (sheeple-issues &optional (classify-by '(status name)))
   (sheeple:with-properties (issues) sheeple-issues
     (loop with classes = (make-hash-table :test 'equalp)
           for issue across issues
           do (sheeple:with-properties (fields) issue
                (loop for classification in classify-by
                      for thing = (sheeple:property-value fields classification)
                          then (sheeple:property-value thing classification)
                      finally (push issue (gethash thing classes))))
           finally (return classes))))

(defun show-issues (sheeple-issues)
  (sheeple:with-properties (issues) sheeple-issues
    (loop for issue across issues
          do (show-issue-short issue))))

(defun show-issue-short (issue)
  (sheeple:with-properties (key fields self) issue
    (sheeple:with-properties (summary status) fields
      (sheeple:with-properties ((status-name name)) status
        (format t "~&~a ~a (~a)~%" self key status-name))
      (show-summary summary)
      (fresh-line))))

(defun show-issue (issue)
  (declare (optimize (debug 3)))
  (sheeple:with-properties (fields key id self) issue
    (sheeple:with-properties (summary description reporter creator assignee status labels) fields
      (sheeple:with-properties ((status-name name)) status
        (format t "~a (~a) <~a>~%" key status-name self))

      (show-summary summary)

      (show-person reporter "Reporter")
      (show-person creator "Creator")
      (when assignee
        (show-person assignee "Assignee"))

      (when (< 0 (length labels))
        (show-labels labels))

      (when description
        (show-description description)))))

(defun show-issuetype (issuetype)
  (sheeple:with-properties (description name) issuetype
    (pprint-logical-block (*standard-output* (tokens description))
      (pprint-indent :block 15 *standard-output*)
      (pprint-exit-if-list-exhausted)
      (format *standard-output* "~4t~10@a: " name)
      (loop
        (princ (pprint-pop))
        (pprint-exit-if-list-exhausted)
        (pprint-newline :fill *standard-output*)
        (princ #\space)))))

(defun show-project (project)
  (sheeple:with-properties (name key issuetypes) project
    (format t "~a: ~a~%" key name)
    (loop for issuetype across issuetypes
          do (show-issuetype issuetype)
          do (terpri))))

(defun show-projects (projects)
  (sheeple:with-properties (projects) projects
    (loop for project across projects
          do (show-project project))))