APIKitのRequestをProtocol用いてまとめてみた


# はじめに
プロジェクトの中で初めてAPIKitを触ったので備忘録として書き残しておきます。

### 環境
- Swift 4.1.2
- APIKit 3.2.1

# 実装

## 1. Protocolの作成

```swift:MyRequest.swift
import Foundation
import APIKit

protocol MyRequest: Request { }
```

## 2. URLとHeaderを持つように拡張

```swift:MyRequest.swift
extension MyRequest {
    var baseURL: URL {
        return URL(string: BASE_URL)!
    }
    var headerFields: [String: String] {
        guard let accessToken = MyAppLocalData.authToken() else {
            return [:]
        }
        return ["X-Authentication-Token": accessToken]
    }
}
```

`MyAppLocalData.authToken()`は`UserDefaults`を用いた以下のような実装。

```swift:MyAppLocalData.swift
enum LocalDataKey: String {
    case authToken = "AuthToken"
}

class MyAppLocalData {
    class func authToken() -> String? {
        return UserDefaults.standard.string(forKey: LocalDataKey.authToken.rawValue)
    }

    class func setAuthToken(_ token: String?) {
        if token == nil {return}
        UserDefaults.standard.set(token!, forKey: LocalDataKey.authToken.rawValue)
        UserDefaults.standard.synchronize()
    }
}
```

## 3. Responseについて拡張

```swift:MyRequest.swift
extension MyRequest where Response: Decodable {
    var dataParser: DataParser {
        return DecodableDataParser()
    }
    
    func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {
        guard let data = object as? Data else {
            throw ResponseError.unexpectedObject(object)
        }
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        
        if #available(iOS 10.0, *) {
            decoder.dateDecodingStrategy = .iso8601
        } else {
            // Fallback on earlier versions
        }

        // Avoid decording error when "204 no content" etc
        if data.count == 0 {
            let emptyJson = "{}"
            return try decoder.decode(Response.self, from: emptyJson.data(using: .utf8)!)
        } else {
            return try decoder.decode(Response.self, from: data)
        }
    }
}
```

`DecodableDataParser()`は以下のようなクラス。

```swift:MyRequest.swift
final class DecodableDataParser: DataParser {
    var contentType: String? {
        return "application/json"
    }
    
    func parse(data: Data) throws -> Any {
        return data
    }
}
```

## 4. ResultをEnumで表現

```swift:MyRequest.swift
enum Result<T> {
    case success(T)
    case failure(Error)
}
```

# 使用例

今回は以下のような構造を持つ`User`についての使用例を挙げます。

```swift:User.swift
struct User: Decodable {
    let id: Int
    let name: String
    let email: String
    let tel: String
}
```

また、新規作成や編集用に以下のような構造の`EditingUser`も使用します。

```swift:EditingUser.swift
struct EditingUser: Decodable {
    var id: Int?
    var name: String?
    var email: String?
    var tel: String?
}
```

## Repository

```swift:UserRepository.swift
import Foundation
import APIKit

struct UserRepository {
    /// Userの登録
    func post(user: EditingUser, handler: @escaping (Result<User>) -> Void) {
        Session.send(UserPostRequest(user: user)) { result in
            switch result {
            case .success(let response):
                handler(.success(response.data))
            case .failure(let error):
                handler(.failure(error))
            }
        }
    }
    /// Userの更新
    func patch(user: EditingUser, handler: @escaping (Result<User>) -> Void) {
        Session.send(UserPatchRequest(user: user)) { result in
            switch result {
            case .success(let response):
                handler(.success(response.data))
            case .failure(let error):
                handler(.failure(error))
            }
        }
    }
    /// Userの削除
    func delete(id: Int, _ handler: @escaping (Result<Any>) -> Void) {
        Session.send(UserDeleteRequest(id: id)) { result in
            switch result {
            case .success(let response):
                handler(.success(response))
            case .failure(let error):
                handler(.failure(error))
            }
        }
    }
    /// Userの取得
    func get(id: Int, handler: @escaping (Result<User>) -> Void) {
        Session.send(UserGetRequest(id: id)) { result in
            switch result {
            case .success(let response):
                handler(.success(response.data))
            case .failure(let error):
                handler(.failure(error))
            }
        }
    }
    
    // MARK: - POST
    // MyRequestプロトコルを使用！！
    struct UserPostRequest: MyRequest {
        typealias Response = UserPostResponse
        let user: EditingUser
        let method: HTTPMethod = .post
        let path: String = "/users"
        var bodyParameters: BodyParameters? {
            var params: [MultipartFormDataBodyParameters.Part] = []
            
            // required params
            guard let name = user.name, let email = user.email else { return nil }
            params.append(try! MultipartFormDataBodyParameters.Part(
                value: name,
                name: "name"))
            params.append(try! MultipartFormDataBodyParameters.Part(
                value: email,
                name: "email"))
            
            // optional params
            if let tel = user.tel {
                params.append(try! MultipartFormDataBodyParameters.Part(
                    value: tel,
                    name: "tel"))
            }
            
            return MultipartFormDataBodyParameters(parts: params)
        }
    }
    
    struct UserPostResponse: Decodable {
        let status: String
        let data: User
    }
    
    // MARK: - PATCH
    struct UserPatchRequest: MyRequest {
        typealias Response = UserPatchResponse
        let user: EditingUser
        let method: HTTPMethod = .patch
        var path: String {
            guard let id = user.id else { return "" }
            return "/users/\(id)"
        }
        var bodyParameters: BodyParameters? {
            var params: [MultipartFormDataBodyParameters.Part] = []
            
            // required params
            guard let name = user.name, let email = user.email else { return nil }
            params.append(try! MultipartFormDataBodyParameters.Part(
                value: name,
                name: "name"))
            params.append(try! MultipartFormDataBodyParameters.Part(
                value: email,
                name: "email"))
            
            // optional params
            if let tel = user.tel {
                params.append(try! MultipartFormDataBodyParameters.Part(
                    value: tel,
                    name: "tel"))
            }
            
            return MultipartFormDataBodyParameters(parts: params)
        }
    }
    
    struct UserPatchResponse: Decodable {
        let status: String
        let data: User
    }
    
    // MARK: - DELETE
    struct UserDeleteRequest: MyRequest {
        typealias Response = UserDeleteResponse
        let id: Int
        let method: HTTPMethod = .delete
        var path: String {
            return "/users/\(id)"
        }
    }
    
    struct UserDeleteResponse: Decodable {
        let status: String?
    }
    
    // MARK: - GET
    struct UserGetRequest: MyRequest {
        typealias Response = UserGetResponse
        let id: Int
        let method: HTTPMethod = .get
        var path: String {
            return "/users/\(id)"
        }    
    }
    
    struct UserGetResponse: Decodable {
        let status: String
        let data: User
    }
}
```

## Repositoryの使用例
getの場合のみ書いておきます。他の場合も同様に使用できます。

```swift
let repository = EventRepository()
repository.get(id: self.id) { [unowned self] result in
    switch result {
    case .success(let user):
        self.user = user
    case .failure(let error):
        print(error)
    }
}
```

# おわりに
少し長くなってしまいましたが、APIKitを用いる時はこれを元に実装していこうかなあと思います。
もっと良い書き方あれば編集リクエストガンガン送ってください。
コードは[GitHubGist](https://gist.github.com/RichGreenTea/36364312285130f71f9efee02a953199)にもあります。