Browse code
Initial commit: works
fiddlerwoaroof authored on 18/04/2016 21:53:30
Showing 6 changed files
Showing 6 changed files
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) |