APIKitとPromiseKitでDeepL APIをMix-in(Trait)

# はじめに
賢い翻訳AIのDeepLを使うにあたり、タイプセーフな通信ライブラリAPIKitの存在を知ったので、PromiseKitと一緒に使ってみました。DeepLのAuth Keyをクライアントに持たせるのは危ういので実際にはこの方法は使わないですが、Alamofireから乗り換えようかなと思ってます。

- DeepL API
50万文字/月までの翻訳は無料で使えるステキなry。
- APIKit
タイプセーフなAPIプログラミングを可能としてくれるステキなry。
- PromiseKit
非同期処理を書くときにクロージャーの入れ子から開放してくれるステキなry。

# Decodable準拠
DeepL APIのレスポンスは以下のようなJSON形式なので、JSONDecoderでdecodeできるように定義します。

```json:DeepLのレスポンス
{
	"translations": [{
		"detected_source_language":"EN",
		"text":"Hallo, Welt!"
	}]
}
```

```swift: Decodable準拠
struct DeepLResponse: Decodable {
    let translations: [Translation]
}

struct Translation: Decodable {
    let text: String
    let detectedSourceLanguage: String
}
```

# DataParser準拠
DataParser準拠のParserを定義します。contentTypeは"application/json"、parseメソッドでJSONDecoderを使用します。DeepLResponseタイプはcamel caseで、DeepLのレスポンスはsnake caseなので、keyDecodingStrategyにconvertFromSnakeCaseを指定します。

```swift
final class JSONDecodableDataParser<T:Decodable>:DataParser {
    var contentType:String? {
        return "application/json"
    }

    func parse(data:Data) throws -> Any {
        let jsonDecoder = JSONDecoder()
        jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
        return try jsonDecoder.decode(T.self, from:data)
    }
}
```
# Request準拠
DeepL APIの呼び出しに合わせてリクエストプロトコルを定義します。
Content-Typeは、application/x-www-form-urlencodedです。

```text:Bodyの例
auth_key=your_auth_key&text=Hello, world&target_lang=DE
```

GETメソッドも対応していますが、安全のためPOSTメソッドを使用します。
POSTメソッドの場合は、bodyParametersでFormURLEncodedBodyParametersを使えば良いようです。

```swift: DeepLRequestの実装
struct DeepLRequest: Request {
    typealias Response = DeepLResponse
    
    let authKey:String
    let text:String
    let targetLang:String
    let sourceLang:String?
    
    var baseURL:URL {
        URL(string:"https://api-free.deepl.com")!
    }
    var method:HTTPMethod {
        .post
    }
    var path:String {
        "/v2/translate"
    }
    var bodyParameters:BodyParameters? {
        var params = ["auth_key":authKey,
                      "text":text,
                      "target_lang":targetLang]
        if let sourceLang = sourceLang {
            params["source_lang"] = sourceLang
        }
        return FormURLEncodedBodyParameters(formObject:params)
    }

    var dataParser:DataParser {
        JSONDecodableDataParser<Response>()
    }

    func response(from object:Any, urlResponse:HTTPURLResponse) throws -> Response {
        guard let object = object as? Response else {
            throw ResponseError.unexpectedObject(object)
        }
        return object
    }
}
```

# Mix-in
PromiseKitを使ってDeepLApiCallableプロトコルを定義します。

```swift
protocol DeepLApiCallable {
    func translate(text:String, targetLang:String, sourceLang:String?) -> Promise<String>
}

extension DeepLApiCallable {
    func translate(text:String, targetLang:String, sourceLang:String?=nil) -> Promise<String> {
        return Promise<String> { resolver in
            let request = DeepLRequest(authKey:your_auth_key, text:text, targetLang:targetLang, sourceLang:sourceLang)
            Session.send(request) { result in
                switch result {
                case .success(let deepLResponse):
                    resolver.fulfill(deepLResponse.translations[0].text)
                case .failure(let error):
                    resolver.reject(error)
                }
            }
        }
    }
}

```

# 使用例

```swift
struct SomeClass:DeepLApiCallable {...}
let someClass = SomeClass()

firstly {
    someClass.translate(text:"こんにちは", targetLang:"EN")
}.done { text in
    // 翻訳結果を利用
}.catch { error in
    // エラー処理
}
```