FeedbackManでJIRAチケットを作りたかったのでやってみた

##はじめに
こんにちは、nirazoです。ホットペッパービューティのアプリの中の人です。

先日、弊社リポジトリで[FeedbackMan](https://github.com/recruit-lifestyle/FeedbackMan)というライブラリを公開しました（not宣伝）。

![capture.gif](https://qiita-image-store.s3.amazonaws.com/0/13042/5e7f203c-1c72-b73d-df5e-43a21b6210a2.gif)
↑こんなやつです


アプリに組み込むと簡単にキャプチャを撮ってSlackにフィードバックが送信できるという素敵ライブラリなのですが、使ってみて思いました。
「普段バグチケットはJIRAで管理してるし、明らかにバグだったらさっさとチケットを作ってしまいたい…」

ということで、FeedbackManを拡張して簡単にJIRAチケットが切れるようにしてみました。

## やりたいこと
上のgifのdebugボタンを押した際に出てくるModalから、スクリーンキャプチャを添付したJIRAチケットを作ります。

<img width="800" alt="スクリーンショット 2017-12-15 2.37.01.png" src="https://qiita-image-store.s3.amazonaws.com/0/13042/3ff2d175-3b3a-cd83-012b-8156c18f782a.png">
↑こんなチケットを作る


## どうやるのか
JIRAには[公開されているAPI](https://docs.atlassian.com/software/jira/docs/api/REST/cloud)が存在するのでこちらを使います。
ちなみに上記リンクはJIRA CloudのAPIで、JIRA ServerのAPIは別途[こちら](https://docs.atlassian.com/jira/REST/server/)にあります。
差分はちゃんと見てないので宿題ということで…

## 作ってみた

### 事前準備
JIRAプロジェクトが無ければ始まらないので、プロジェクトを作成しておきます。
SwiftJiraSampleなどという名前にしたらプロジェクトキーが「SWIF」という残念な感じになりました。
<img width="1227" alt="スクリーンショット 2017-12-15 2.29.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/13042/9ad9fbea-b2d4-1b52-a8f1-db580b46b829.png">

### チケットを切る
まずはシンプルにチケットを切るところです。
Issueを切るためのAPIは[こちら](https://docs.atlassian.com/software/jira/docs/api/REST/1000.1584.0/#api/2/issue-createIssue)。
チケットのタイトル（summary）、説明文(description)、報告者（reporter）などMustなものから、見積もり時間等（timetracking）、期限（duedate）など、あらゆる項目を設定できます。
ただしファイルを添付した状態でいきなりチケット作成はできないようなので、いったんシンプルにチケット作成だけ行います。

#### APIの準備
FeedbackManにはAPIプロトコルが用意されているので、JIRA用のAPIをenumで作ります。
チケット作成用のcreateIssueとファイル添付用のattachFileを用意しています。

```swift:API.swift
protocol API {
    var buildURL: String { get }
    var baseURL: String { get }
    var path: String { get }
    var parameters: [String: Any] { get }
    var body: Data { get }
}

enum JiraAPI: API {
    
    case createIssue()
    case attachFile(issueKey: String, image: UIImage)
    
    static let host = JiraManager.JiraConstants.host
    static let projectID = JiraManager.JiraConstants.projectID
    var buildURL: String {
        return "\(baseURL)\(path)"
    }
    
    var baseURL: String {
        return "https://\(JiraAPI.host)"
    }
    
    var path: String {
        switch self {
        case .createIssue(): return "/rest/api/2/issue"
        case .attachFile(issueKey: let issueKey, _): return "/rest/api/2/issue/\(issueKey)/attachments"
        }
    }
    
    var parameters: [String: Any] {
        switch self {
        case .createIssue(): return [:]
        case .attachFile(_, image: let image):
            let params: [String: Any] = ["file": image]
            return params
        }
    }
    
    var body: Data {
        var project = [String: String]()
        project["id"] = JiraAPI.projectID
        var issueType = [String: String]()
        issueType["id"] = "10004"
        
        var body = [String: Any]()
        body["project"] = project
        body["summary"] = "Sample bug"
        body["description"] = "バグが発生しました！"
        body["issuetype"] = issueType
        
        var fields = [String: Any]()
        fields["fields"] = body
        
        let jsonData = try! JSONSerialization.data(withJSONObject: fields, options: [])
        return jsonData
    }
}
```

projectIDやissueTypeがわからない！というときは[こちらの記事](https://qiita.com/yo1000/items/bf13c94f9fb1cbbae7bb)を参考にすると良いかと思います。
body部分はAPIドキュメントのEXAMPLEを参考にして、Dictionaryを上手く組み立ててJSONの形にします。

#### APIクライアントの準備
```swift:APIClient.swift
static func createJiraIssue(api:JiraAPI, completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
        let url = URL(string: api.buildURL)
        var request = URLRequest(url: url!)
        request.httpMethod = "POST"
        let login = "{email}:{pass}".data(using: .utf8)
        let base64Login = login!.base64EncodedString(options: [])
        request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
        request.addValue("application/json", forHTTPHeaderField: "Accept")
        request.addValue("Basic \(base64Login)", forHTTPHeaderField: "Authorization")
        request.httpBody = api.body
        let task = URLSession.shared.dataTask(with: request, completionHandler: completion)
        task.resume()
    }
```
特段変わったことはしてないですね。
Content-Type等を適切に指定し、bodyにJSON(Data)を渡すだけです。
こいつを呼んであげれば、チケットが作成されます。

```swift
func createJiraIssue(completion: @escaping (Result<Bool, APIError>) -> Void) {
        APIClient.createJiraIssue(api: JiraAPI.createIssue(), completion: { data, response, error in
            if let data = data, let response = response {
                completion(.success(true))
            } else {
                completion(.failure(.other))
            }
        })
    }
```

![スクリーンショット 2017-12-15 19.20.33.png](https://qiita-image-store.s3.amazonaws.com/0/13042/6bd376b7-eafa-fcad-c5b3-bda73e8a6a44.png)
シンプルなチケットが作成されました。
簡単ですね！


### チケットに画像を添付する
チケット自体は作成できたのですが、キャプチャ画像がないと何のこっちゃわかりません。というわけでキャプチャを貼りましょう。

やり方としては、チケット作成のコールバック内で、作成したチケットのkeyを取得してそのまま[画像添付用のAPI](https://docs.atlassian.com/software/jira/docs/api/REST/1000.1584.0/#api/2/issue/{issueIdOrKey}/attachments-addAttachment)をコールします。

```swift
func attachImageToIssue(issueKey: String, image: UIImage, completion: @escaping (Result<Bool, APIError>) -> Void) {
    let jiraApi = JiraAPI.attachFile(issueKey: issueKey, image: image)
    
    APIClient.multipartPost(api: jiraApi, completion:
        { data, response, error in
            if let _ = data, let response = response {
                completion(.success(true))
            } else {
                completion(.failure(.other))
            }
    }
    )
}
```
画像添付用のメソッドです。
画像を送信することになるので、multipartで送ります。

```swift
    static func multipartPost(api: API, completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
        
        let url = URL(string: api.buildURL)
        let uniqueId = ProcessInfo.processInfo.globallyUniqueString
        let boundary = "---\(uniqueId)" //値を長くしすぎないように！
        var request = URLRequest(url: url!)
        request.httpMethod = "POST"
        let login = "\(JiraManager.JiraConstants.mail):\(JiraManager.JiraConstants.pass)".data(using: .utf8)
        let base64Login = login!.base64EncodedString(options: [])
        request.addValue("no-check", forHTTPHeaderField: "X-Atlassian-Token")
        request.addValue("Basic \(base64Login)", forHTTPHeaderField: "Authorization")
        request.addValue("application/json", forHTTPHeaderField: "Accept")
        request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
        request.httpBody = createBody(parameters: api.parameters, boundary: boundary)
        let task = URLSession.shared.dataTask(with: request, completionHandler: completion)
        task.resume()
    }

```
ここで1個ハマったこととして、Content-Typeヘッダの値を長くしすぎるとAPIのレスポンスで何故か500が返ってくるため、長くしすぎないように注意して下さい（FeedbackManの元々のコードのままでは送れなかった…）。

チケット作成リクエストのリクエストのコールバックでissueKeyを取得し、そのチケットに画像ファイルを添付するコードが下記です。

```swift
func createJiraIssue(image: UIImage?, completion: @escaping (Result<Bool, APIError>) -> Void) {
    APIClient.createJiraIssue(api: JiraAPI.createIssue(), completion: { [weak self] data, response, error in
        if let data = data, let response = response {
            do {
                let json = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as? [String: Any]
                if let json = json, let key = json["key"] as? String, let image = image {
                    self?.attachImageToIssue(issueKey: key, image: image) { result in
                        DispatchQueue.main.async {
                            switch result {
                            case .success:
                                completion(.success(true))
                            case .failure:
                                completion(.failure(.other))
                            }
                        }
                    }
                }
            } catch {
                completion(.failure(.other))
            }
            completion(.success(true))
        } else {
            completion(.failure(.other))
        }
    })
}
```
ネストが深くなってしまっているのはご容赦下さい。
これで、FeedbackManから画像ファイル付きのバグチケット作成ができました！

![FeedbackJira.gif](https://qiita-image-store.s3.amazonaws.com/0/13042/67d98dd9-4802-7be0-f8a5-5b2c00c16fd7.gif)




## 最後に
こうやって簡単にJIRAチケットが作れると便利ですね！JIRAチケットだけでなく、他にも様々なプロジェクト管理ツールにも対応すると業務用途でもどんどん導入できるかと思います。
まだまだ荒いコードで本家FeedbackManにPull Requestを送ったりはしていないですが、ソースコードを[github](https://github.com/nirazo/FeedbackMan)に置いています。興味のある方は見てみて下さい！