git.fiddlerwoaroof.com
Browse code

Initial commit: works

fiddlerwoaroof authored on 18/04/2016 21:53:30
Showing 6 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,2 @@
1
+An implementation of part of JIRA's API and a simple client for listing and searching issues.
2
+
0 3
new file mode 100644
... ...
@@ -0,0 +1,18 @@
1
+;;;; jira-api.asd
2
+
3
+(asdf:defsystem #:jira-api
4
+  :description "Describe jira-api here"
5
+  :author "Your Name <your.name@example.com>"
6
+  :license "Specify license here"
7
+  :depends-on (#:fwoar.lisputils
8
+               #:alexandria
9
+               #:serapeum
10
+               #:drakma
11
+               #:xhtmlambda
12
+               #:yason
13
+               #:sheeple)
14
+  :serial t
15
+  :components ((:file "package")
16
+               (:file "prompt")
17
+               (:file "jira-api")))
18
+
0 19
new file mode 100644
... ...
@@ -0,0 +1,205 @@
1
+;;;; jira-api.lisp
2
+
3
+(in-package #:jira-api)
4
+
5
+;;; "jira-api" goes here. Hacks and glory await!
6
+
7
+; curl -u user:password https://atomampd.atlassian.net/rest/api/2/issue/ATOMOS-212 | jq .
8
+(defparameter *endpoint* "https://socraticum.atlassian.net/rest/api/2/")
9
+
10
+(defun api-get-call (auth method &rest parameters)
11
+  "Connect to a GET REST endpoint specified by method and return a stream from
12
+   which the response can be read."
13
+  (let ((drakma:*text-content-types* (acons "application" "json" drakma:*text-content-types*)))
14
+    (drakma:http-request (puri:merge-uris method *endpoint*)
15
+                         :parameters (alexandria:plist-alist parameters)
16
+                         :basic-authorization auth
17
+                         :want-stream t)))
18
+
19
+(defun api-post-call (auth method post-data)
20
+  "Connect to a GET REST endpoint specified by method and return a stream from
21
+   which the response can be read."
22
+  (let ((drakma:*text-content-types* (acons "application" "json" drakma:*text-content-types*)))
23
+    (drakma:http-request (puri:merge-uris method *endpoint*)
24
+                         :method :POST
25
+                         :content-type "application/json"
26
+                         :content post-data
27
+                         :basic-authorization auth
28
+                         :want-stream t)))
29
+
30
+(defun get-issues (auth)
31
+  (api-get-call auth "search"))
32
+
33
+(defun get-issue (auth key &rest params &key expand)
34
+  (declare (ignore expand))
35
+  (apply 'api-get-call auth (format nil "issue/~a" key) params))
36
+
37
+(defun get-projects (auth)
38
+  (api-get-call auth "issue/createmeta"))
39
+
40
+(defun make-issue (project-key summary &key (description nil description-p) (issue-type "Bug"))
41
+  (let* ((issue (make-hash-table :test 'equal))
42
+         (project (alist-string-hash-table `(("key" . ,project-key))))
43
+         (issue-type (alist-string-hash-table `(("name" . ,issue-type)))))
44
+    (setf (gethash "fields" issue)
45
+          (alist-string-hash-table
46
+            `(("project" . ,project)
47
+              ("summary" . ,summary)
48
+              ,@(when description-p
49
+                  (list
50
+                    (cons "description" description)))
51
+              ("issuetype" . ,issue-type))))
52
+    issue))
53
+
54
+(defun post-issue (auth issue)
55
+  (let-each (:be *)
56
+    issue
57
+    (with-output-to-string (s)
58
+      (yason:encode * s))
59
+    (api-post-call auth "issue" *)))
60
+
61
+(defun post-issue-from-fields (auth project-key summary &key
62
+                        (description nil description-p)
63
+                        (issue-type nil issue-type-p))
64
+  (let ((optional-arguments '()))
65
+    (when issue-type-p
66
+      (push issue-type optional-arguments))
67
+    (when description-p
68
+      (push description optional-arguments))
69
+
70
+    (let-each (:be *)
71
+      (apply #'make-issue project-key summary optional-arguments)
72
+      (post-issue auth *))))
73
+
74
+(defun read-issue (&optional (iostream *query-io*))
75
+  (let ((project-key (prompt :project-key iostream))
76
+        (summary (prompt :summary iostream))
77
+        (description (optional-prompt iostream :description "Add a description? ")))
78
+    (apply #'make-issue project-key summary
79
+           (list*
80
+             (when description
81
+               (list description))))))
82
+
83
+(deftype vector-of-objects () '(vector (or hash-table sheeple:object) *))
84
+
85
+(defun json2sheeple (json)
86
+  (labels
87
+    ((handle-parsed (parsed-json)
88
+       (typecase parsed-json
89
+         (vector-of-objects (map 'vector #'handle-parsed parsed-json))
90
+         (hash-table
91
+           (let ((result (sheeple:object)))
92
+             (loop for json-prop being the hash-keys of parsed-json using (hash-value json-value)
93
+                   do (setf (sheeple:property-value result
94
+                                                    (intern (string-upcase json-prop) :jira-api))
95
+                            (typecase json-value
96
+                              (hash-table (handle-parsed json-value))
97
+                              (vector-of-objects (map 'vector #'handle-parsed json-value))
98
+                              (t json-value)))
99
+                   finally (return result))))
100
+         (t parsed-json))))
101
+    (let ((yason:*parse-json-arrays-as-vectors* t))
102
+      (handle-parsed (yason:parse json)))))
103
+
104
+(defun show-person (person title)
105
+  (sheeple:with-properties (displayname emailaddress) person
106
+    (format t "~&~4t~a: \"~a\" <~a>~%" title displayname emailaddress)))
107
+
108
+(defun show-labels (labels)
109
+  (format t "~&~4tLabels: ~{~a~}~%" (coerce labels 'list)))
110
+
111
+(defun show-description (description)
112
+  (pprint-logical-block (*standard-output* (mapcar (compose 'tokens 'trim-whitespace)
113
+                                                 (lines description)))
114
+    (pprint-indent :block 4 *standard-output*)
115
+    (pprint-newline :mandatory *standard-output*)
116
+    (loop
117
+      (pprint-exit-if-list-exhausted)
118
+      (let ((line (pprint-pop)))
119
+        (pprint-logical-block (*standard-output* line)
120
+          (loop
121
+            (princ (pprint-pop) *standard-output*)
122
+            (pprint-exit-if-list-exhausted)
123
+            (princ #\space *standard-output*)
124
+            (pprint-indent :block 3)
125
+            (pprint-newline :fill *standard-output*)))
126
+        (pprint-newline :mandatory *standard-output*)))))
127
+
128
+(defun show-summary (summary)
129
+  (pprint-logical-block (*standard-output* (funcall (compose 'tokens 'trim-whitespace) summary))
130
+    (pprint-indent :block 8 *standard-output*)
131
+    (pprint-exit-if-list-exhausted)
132
+    (format *standard-output* "~4tSummary: ")
133
+    (loop
134
+      (princ (pprint-pop))
135
+      (pprint-exit-if-list-exhausted)
136
+      (pprint-newline :fill *standard-output*)
137
+      (princ #\space))))
138
+
139
+(defun classify-issues (sheeple-issues &optional (classify-by '(status name)))
140
+   (sheeple:with-properties (issues) sheeple-issues
141
+     (loop with classes = (make-hash-table :test 'equalp)
142
+           for issue across issues
143
+           do (sheeple:with-properties (fields) issue
144
+                (loop for classification in classify-by
145
+                      for thing = (sheeple:property-value fields classification)
146
+                          then (sheeple:property-value thing classification)
147
+                      finally (push issue (gethash thing classes))))
148
+           finally (return classes))))
149
+
150
+(defun show-issues (sheeple-issues)
151
+  (sheeple:with-properties (issues) sheeple-issues
152
+    (loop for issue across issues
153
+          do (show-issue-short issue))))
154
+
155
+(defun show-issue-short (issue)
156
+  (sheeple:with-properties (key fields self) issue
157
+    (sheeple:with-properties (summary status) fields
158
+      (sheeple:with-properties ((status-name name)) status
159
+        (format t "~&~a ~a (~a)~%" self key status-name))
160
+      (show-summary summary)
161
+      (fresh-line))))
162
+
163
+(defun show-issue (issue)
164
+  (declare (optimize (debug 3)))
165
+  (sheeple:with-properties (fields key id self) issue
166
+    (sheeple:with-properties (summary description reporter creator assignee status labels) fields
167
+      (sheeple:with-properties ((status-name name)) status
168
+        (format t "~a (~a) <~a>~%" key status-name self))
169
+
170
+      (show-summary summary)
171
+
172
+      (show-person reporter "Reporter")
173
+      (show-person creator "Creator")
174
+      (when assignee
175
+        (show-person assignee "Assignee"))
176
+
177
+      (when (< 0 (length labels))
178
+        (show-labels labels))
179
+
180
+      (when description
181
+        (show-description description)))))
182
+
183
+(defun show-issuetype (issuetype)
184
+  (sheeple:with-properties (description name) issuetype
185
+    (pprint-logical-block (*standard-output* (tokens description))
186
+      (pprint-indent :block 15 *standard-output*)
187
+      (pprint-exit-if-list-exhausted)
188
+      (format *standard-output* "~4t~10@a: " name)
189
+      (loop
190
+        (princ (pprint-pop))
191
+        (pprint-exit-if-list-exhausted)
192
+        (pprint-newline :fill *standard-output*)
193
+        (princ #\space)))))
194
+
195
+(defun show-project (project)
196
+  (sheeple:with-properties (name key issuetypes) project
197
+    (format t "~a: ~a~%" key name)
198
+    (loop for issuetype across issuetypes
199
+          do (show-issuetype issuetype)
200
+          do (terpri))))
201
+
202
+(defun show-projects (projects)
203
+  (sheeple:with-properties (projects) projects
204
+    (loop for project across projects
205
+          do (show-project project))))
0 206
new file mode 100644
... ...
@@ -0,0 +1,12 @@
1
+;;;; package.lisp
2
+
3
+(defpackage #:jira-api.cli
4
+  (:use :cl :anaphora :alexandria :serapeum :fw.lu)
5
+  (:export #:prompt-for-lines
6
+           #:prompt-for-line
7
+           #:optional-prompt
8
+           #:prompt))
9
+
10
+(defpackage #:jira-api
11
+  (:use #:cl #:serapeum #:alexandria #:fw.lu #:jira-api.cli))
12
+
0 13
new file mode 100644
... ...
@@ -0,0 +1,49 @@
1
+(in-package #:jira-api.cli)
2
+
3
+(defparameter *prompt-stream* (make-synonym-stream '*query-io*))
4
+
5
+(defun prompt-for-line (stream prompt &rest args)
6
+  (apply #'format stream prompt args)
7
+  (force-output stream)
8
+  (read-line stream nil :done))
9
+(trace prompt-for-line)
10
+
11
+(defun prompt-for-lines (stream initial-prompt continuation-prompt &rest args)
12
+  "Not finished ... see :description below"
13
+  (loop with lines = (make-array 10 :adjustable t :fill-pointer 0)
14
+        for line = (prompt-for-line stream initial-prompt args)
15
+        then (prompt-for-line stream continuation-prompt)
16
+        for trim-line = (trim-whitespace line)
17
+        until (equal trim-line ".")
18
+        do (vector-push-extend trim-line lines)
19
+        finally (return (string-join lines #\space))))
20
+
21
+(defun prompt-for-boolean (stream prompt &rest args)
22
+  (let ((input (string-downcase (prompt-for-line stream prompt args))))
23
+    (ecase (elt input 0)
24
+      (#\y t)
25
+      (#\n nil))))
26
+
27
+(defgeneric prompt (field-name &optional stream))
28
+
29
+(defun optional-prompt (stream field-name test-prompt &rest args)
30
+  (when (apply #'prompt-for-boolean stream test-prompt args)
31
+    (prompt field-name stream)))
32
+
33
+
34
+(defmethod prompt ((field-name (eql :project-key)) &optional (stream *prompt-stream*))
35
+  (funcall #'string-upcase (prompt-for-line stream "Project Key (e.g. ATOMOS)? ")))
36
+
37
+(defmethod prompt ((field-name (eql :summary)) &optional (stream *prompt-stream*))
38
+  (prompt-for-line stream "Summary? "))
39
+
40
+(defmethod prompt ((field-name (eql :description)) &optional (stream *prompt-stream*))
41
+  (loop with lines = (make-array 10 :adjustable t :fill-pointer 0)
42
+        for line = (prompt-for-line stream "Enter a description end with \".\" on its own line~%? ")
43
+        then (prompt-for-line stream "? ")
44
+        for trim-line = (when (stringp line)
45
+                          (trim-whitespace line))
46
+        until (or (eql trim-line :done) (equal trim-line "."))
47
+        do (vector-push-extend trim-line lines)
48
+        finally (return (string-join lines #\newline))))
49
+
0 50
new file mode 100755
... ...
@@ -0,0 +1,87 @@
1
+#!/usr/bin/sbcl --script
2
+(require :sb-posix)
3
+(load #p"~/quicklisp/setup.lisp")
4
+(eval-when (:load-toplevel :compile-toplevel :execute)
5
+  (push (truename ".") asdf:*central-registry*)
6
+  (ql:quickload :ubiquitous)
7
+  (sb-posix:setenv "CC" "gcc" 1)
8
+  (ql:quickload :net.didierverna.clon)
9
+  (ql:quickload :jira-api))
10
+
11
+(defpackage #:jira-api.client
12
+  (:use #:cl #:serapeum #:alexandria #:fw.lu #:jira-api.cli #:jira-api #:net.didierverna.clon))
13
+
14
+(in-package #:jira-api.client)
15
+(defparameter *version* (format nil "0.1-init")) 
16
+
17
+(defparameter *endpoint-template* "https://~a.atlassian.net/rest/api/2/")
18
+
19
+(eval-when (:compile-toplevel :load-toplevel :execute)
20
+  (ubiquitous:restore :jira-api))
21
+
22
+(defsynopsis (:postfix "ARGUMENTS...")
23
+  (text :contents "A command line client for Jira issues")
24
+  (group (:header "Main actions")
25
+         (flag :short-name "lp" :long-name "list-projects"
26
+               :description "List available JIRA projects")
27
+         (flag :short-name "is"
28
+               :long-name "get-issues"
29
+               :description "list issues")
30
+         (flag :short-name "i"
31
+               :long-name "get-issue"
32
+               :description "show an issue")
33
+         (flag :short-name "pi" :long-name "post-issue"
34
+               :description "post and issue"))
35
+  (group (:header "JIRA options")
36
+         (stropt :long-name "jira-account"
37
+                 :description "The jira account to use."
38
+                 :argument-name "URL-SUBDOMAIN"
39
+                 :default-value (ubiquitous:value :jira :account)))
40
+  (group (:header "Filtering Issues")
41
+         (stropt :short-name "s" :long-name "status"
42
+                 :description "Only show issues with a certain status"))
43
+  (group (:header "Other options")
44
+         (flag :short-name "h" :long-name "help"
45
+               :description "Show this help") 
46
+         (flag :short-name "v" :long-name "version"
47
+               :description "Show the program version")))
48
+
49
+(defvar *auth*)
50
+
51
+(defun main ()
52
+  (ubiquitous:restore :jira-api) 
53
+  (setf *auth* (ubiquitous:value :jira :creds))
54
+  (make-context)
55
+  
56
+  (let ((jira-api::*endpoint* (format nil *endpoint-template* (getopt :long-name "jira-account"))))
57
+    (cond
58
+      ((getopt :long-name "help") (help))
59
+      ((getopt :long-name "version") (format t "~&~a~%" *version*))
60
+      ((getopt :long-name "get-issue") (let ((options (remainder)))
61
+                                         (jira-api::show-issue
62
+                                           (jira-api::json2sheeple
63
+                                             (jira-api::get-issue *auth*
64
+                                                                  (format nil "~a-~a"
65
+                                                                          (car options)
66
+                                                                          (cadr options)))))))
67
+      ((getopt :long-name "get-issues")
68
+       (let ((options (remainder)))
69
+         (let ((issues (jira-api::json2sheeple (jira-api::get-issues *auth*))))
70
+           (alexandria:if-let ((status (getopt :long-name "status")))
71
+             (setf issues 
72
+                   (sheeple:defobject ()
73
+                                      ((jira-api::issues
74
+                                         (apply 'vector
75
+                                                (gethash status
76
+                                                         (jira-api::classify-issues issues))))))))
77
+           (jira-api::show-issues issues))))
78
+      ((getopt :long-name "list-projects") (jira-api::show-projects
79
+                                             (jira-api::json2sheeple
80
+                                               (jira-api::get-projects *auth*))))
81
+      ((getopt :long-name "post-issue") (yason:encode (jira-api::read-issue)))
82
+      (t   (do-cmdline-options (option name value source)
83
+                               (print (list option name value source)))
84
+           (terpri)
85
+           (exit)))))
86
+
87
+(dump "jira-client" main)