[iOS][Swift4.1] Moya+Codable+Slack APIでメッセージ投稿

[Slack API](https://api.slack.com/)使ってメッセージ投稿する方法を調べてて、たまたま同僚から教えてもらった[Moya](!https://github.com/Moya/Moya)が便利そうで、どれだけシンプルになるか試したくなったのでCodableと組み合わせてSlackチャンネルにメッセージ投稿するサンプルコード書いてみた。

https://github.com/atsushijike/Slacker/tree/qiita_43ce0848e13d36ac7baf

* Xcode 9.4.1
* Swift 4.1

# 準備
Slack APIを使用するにはAccess Tokenが必要になる。
token取得周りはレガシーな情報が多いので現時点での最新の取得方法を記載しておきたい。
本来２段階認証して取得すべきだが、今回はTestなのでとりあえず手動で発行する。

## Appの登録
1. https://api.slack.com/apps を開く
<img width="573" alt="slack_api_begin.png" src="https://qiita-image-store.s3.amazonaws.com/0/156085/578302e0-726f-820d-b94c-d850e5ed1733.png">

2. 右上の **Create New App** を押下する
3. アプリ名とWorkspaceを選択して **Create App** を押下する
<img width="418" alt="slack_api_createapp.png" src="https://qiita-image-store.s3.amazonaws.com/0/156085/251c044b-4a1f-36d6-f3b0-88e5cb7e8eb4.png">

## Permission設定してtoken発行
1. *Add features and functionality* でPermissionsを選択する
<img width="519" alt="slack_api_permissions.png" src="https://qiita-image-store.s3.amazonaws.com/0/156085/ca39db16-9b7d-195c-99b6-bf3a2018b425.png">

2. `post.ChatMessage` を使うので *Scope* 部分で **Send messages as user** を選択する
<img width="527" alt="slack_api_scope1.png" src="https://qiita-image-store.s3.amazonaws.com/0/156085/fad7c000-2a3b-6bf1-45b8-a2a95c434682.png">

3. **Save changes** すると *OAuth Tokens & Redirect URLs* にある **Install App to Workspace** が活性化されるのでそれを押下する
<img width="524" alt="slack_api_scope2.png" src="https://qiita-image-store.s3.amazonaws.com/0/156085/f9c08d14-7774-d2bb-4a30-a4a9b2d089c4.png">


4. 許可を求められるので **Authorize** を押す
<img width="410" alt="slack_api_authorize.png" src="https://qiita-image-store.s3.amazonaws.com/0/156085/ee19f8fc-f79e-7e06-7721-17a9a23b7705.png">

5. *OAuth Access Token* が払い出されるので **Copy** しておく
<img width="527" alt="slack_api_accesstoken.png" src="https://qiita-image-store.s3.amazonaws.com/0/156085/a55b694a-e7c8-c390-833a-8bce36a033f0.png">


## メッセージ投稿API
`post.ChatMessage` を使う。
https://api.slack.com/methods/chat.postMessage

## Test
ちゃんと払い出されているかテストしておく。（害のないchannelを指定しましょう）

ブラウザで `https://slack.com/api/chat.postMessage?token=<YOUR_ACCESS_TOKEN>&channel=channel_name&text=Hello%20World!&as_user=true` にアクセス

もしくは以下をコマンド実行してレスポンスを確認する

```
$ curl -X POST -d 'token=<YOUR_ACCESS_TOKEN>&channel=channel_name&text=Hello World!' 'https://slack.com/api/chat.postMessage'
```
レスポンス

```
{"ok":true,"channel":"C1X23XX4X","ts":"1531885188.000090","message":{"type":"message","user":"U12XX3XX4","text":"Hello World!","bot_id":"BXXXXXXX1","ts":"1531885188.000090"}}
```
Private Channel でも Public Channel でもどちらでも問題なさそう。

# 実装
プロジェクトにMoyaを入れる。

Cartfileの記述

```
github "Moya/Moya"
```
frameworkのビルド

```
$ carthage bootstrap --platform iOS
```
プロジェクト > General > Linked Frameworks and Librariesにビルドしたframeworkを追加。
![xcode_linkedframeworks.png](https://qiita-image-store.s3.amazonaws.com/0/156085/6bcb2159-5992-0168-0c7d-b6d9e6279a8c.png)

プロジェクト > Build Phase にRun Scriptにも追加。
![xcode_buildphase.png](https://qiita-image-store.s3.amazonaws.com/0/156085/cba33b10-b123-4b6f-82cc-fe36f1ccf295.png)

今回、Rxは使わないので最低限の Result/Moya/Alamofire だけ追加している。
詳しい追加方法
https://qiita.com/a_jike/items/65147ccc8783f19d4a4a

## APIの定義
`TargetType` プロトコルに準拠する形で定義する。
`var sampleData: Data` はテストコードでモックとして使える。

```Swift:Api.swift
enum Slack {
    case chatPostMessage(token: String, channel: String, text: String, asUser: Bool)
}

extension Slack: TargetType {
    var baseURL: URL { return URL(string: "https://slack.com/api")! }
    var path: String {
        switch self {
        case .chatPostMessage:
            return "/chat.postMessage"
        }
    }
    var method: Method {
        switch self {
        case .chatPostMessage:
            return .post
        }
    }
    var task: Task {
        switch self {
        case .chatPostMessage(let token, let channel, let text, let asUser):
            return .requestParameters(parameters: ["token": token, "channel": channel, "text": text, "as_user": asUser], encoding: URLEncoding.default)
        }
    }
    var headers: [String : String]? { return nil }
    var sampleData: Data {
        let path = Bundle.main.path(forResource: "chatPostMessage", ofType: "json")!
        return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile()
    }
}
```

APIが増えるとcaseも増えてきて次第に見にくくなってくるので、
`TargetType` に準拠したプロトコルを定義して共通処理をそこに移しておき
そのプロトコルに準拠したstructを定義するとAPIごとに記述が纏まる。

```Swift:Api.swift
protocol SlackApiTargetType: TargetType {
}

extension SlackApiTargetType {
    var baseURL: URL { return URL(string: "https://slack.com/api")! }
    var headers: [String : String]? { return nil }
}

enum Slack {
    struct ChatPostMessage: SlackApiTargetType {
        var path: String { return "/chat.postMessage" }
        var method: Method { return .post }
        var task: Task {
            return .requestParameters(parameters: ["token": token, "channel": channel, "text": text, "as_user": asUser], encoding: URLEncoding.default)
        }
        var sampleData: Data {
            let path = Bundle.main.path(forResource: "chatPostMessage", ofType: "json")!
            return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile()
        }
        let token: String
        let channel: String
        let text: String
        let asUser: Bool
        
        init(token: String, channel: String, text: String, asUser: Bool) {
            self.token = token
            self.channel = channel
            self.text = text
            self.asUser = asUser
        }
    }
}
```


## Responseの定義
Codableを使ってcamelCaseで定義しておく。

```Swift:Response.swift
struct PostMessageResponse: Codable {
    let ok: Bool
    let channel: String
    let ts: String
    let message: Message
}

struct Message: Codable {
    let type: String
    let user: String
    let text: String
    let botId: String
    let ts: String
}
```

## Request
Swift4.1からJSONDecoderにsnake_case <-> camelCaseの相互変換してくれる`keyEncodingStrategy` `keyDecodingStrategy` が追加されたのでCodingKeysを定義せずに良くなり更に楽になった。

```Swift:AppDelegate.swift
let provider = MoyaProvider<Slack.ChatPostMessage>()
provider.request(Slack.ChatPostMessage(
    token: "<YOUR_ACCESS_TOKEN>",       // 取得したAccess Token
    channel: "channel_name",            // メッセージ投稿先チャンネル
    text: "Hello World!",               // メッセージ内容
    asUser: true)) { (result) in
    switch result {
    case let .success(response):
        let jsonDecoder = JSONDecoder()
        jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase

        do {
            let messageResponse = try jsonDecoder.decode(MessageResponse.self, from: response.data)
            print(messageResponse)
        } catch {
            print(error.localizedDescription)
        }
    case let .failure(error):
        print(error)
    }
}
```

このままだとリクエスト毎に `MoyaProvider` や `JSONDecoder` を生成するのが面倒なのでこれらも纏める。

```Swift:Api.swift
class Api {
    static let shared = Api()

    func send<Request: SlackApiTargetType>(_ request: Request, completion: @escaping (_ response: Request.Response?, _ error: Moya.MoyaError?) -> Void) -> Void {
        let provider = MoyaProvider<MultiTarget>()
        let target = MultiTarget(request)
        provider.request(target) { (result) in
            switch result {
            case let .success(response):
                let jsonDecoder = JSONDecoder()
                jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
                
                do {
                    let decodedResponse = try response.map(Request.Response.self, using: jsonDecoder, failsOnEmptyData: true)
                    completion(decodedResponse, nil)
                } catch {
                    print(error.localizedDescription)
                }
            case let .failure(error):
                completion(nil, error)
            }
        }
    }
}
```

```Swift:Api.swift
protocol SlackApiTargetType: TargetType {
    associatedtype Response: Codable                    // 追加
}
```

```Swift:Api.swift
enum Slack {
    struct ChatPostMessage: SlackApiTargetType {
        typealias Response = MessageResponse            // 追加
        var path: String { return "/chat.postMessage" }
        var method: Method { return .post }
        var task: Task {
            return .requestParameters(parameters: ["token": token, "channel": channel, "text": text, "as_user": asUser], encoding: URLEncoding.default)
        }
        var sampleData: Data {
            let path = Bundle.main.path(forResource: "chatPostMessage", ofType: "json")!
            return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile()
        }
        let token: String
        let channel: String
        let text: String
        let asUser: Bool
        
        init(token: String, channel: String, text: String, asUser: Bool) {
            self.token = token
            self.channel = channel
            self.text = text
            self.asUser = asUser
        }
    }
}
```

リクエスト

```Swift:AppDelegate.swift
let request = Slack.ChatPostMessage(
    token: "<YOUR_ACCESS_TOKEN>",       // 取得したAccess Token
    channel: "channel_name",            // メッセージ投稿先チャンネル
    text: "Hello World!",               // メッセージ内容
    asUser: true)
Api.shared.send(request) { (response, error) in
    if let response = response {
        print("response: \(response)")
    }
    if let error = error {
        print("error: \(error)")
    }
}
```

Moyaいいですね。enumで実装していくのは長くなりそうで微妙ですが、struct定義に変えれば見やすいし、Alamofireをあまり意識しなくて良いのでかなり少ないコードで済みます。
`MoyaProvider` を継承すればAlamofire.SessionManagerも弄れそうだ。

# 参考
* [Moya](https://github.com/Moya/Moya)
* [Slack API](https://api.slack.com/#read_the_docs)
* [Slack API 推奨Tokenについて](https://qiita.com/ykhirao/items/3b19ee6a1458cfb4ba21)
* [Swift4 + Moya + RxSwift + Codableで作るAPIクライアント](https://qiita.com/kouheiszk/items/46e9a233d9bb227c3b1d)