LoginSignup
2

More than 5 years have passed since last update.

AWS API Gateway(SDK version ≧2.6)の自動生成コードを、ちゃんとコンパイルが通るSwiftyなコードで置き換える

Last updated at Posted at 2017-11-04

Serverlessconf Tokyo 2017 に行ってきました。楽しかったです。
これをきっかけに、AWSのAPI Gatewayを使ってみたのですが、サーバ知識が弱い自分でも特に難しいことなくREST APIを作ることができて、今後のiOSアプリ製作に向けての夢が広がりました。
ただ、使っていて一番気になったのは、API Gatewayのジェネレートしてくれるクライアントコードの質があまり良くないことでした。

2017-11-04 09.44.49.png

API Gatewayでは、ステージエディタという画面から、APIの定義に従ってクライアントコードをジェネレートしてくれます。
しかし、いくつか不満があります。

API Gateway × Swift の不満点

コードの対応しているSDK versionが2.5.3

  • 最新SDK(2.6.5)に追従していない
  • SDKには破壊的変更が入っており、そのままコードを使おうとするとビルドエラーになる

ジェネレートしたコードのままだと、APIGatewayと直接やりとりをする層以外の場所にAWSのSDKの型が漏れ出す

  • リクエスト時の引数として class AWSModel を継承した型が必要
  • パースされるオブジェクトが class AWSModel を継承している
    • 数値が NSNumber だったり色々アレ
    • できれば純粋なstructにしたい
    • 今ならCodableも活用できるはず
  • 非同期処理の実現に AWSTask を使っている
    • 外からのインタフェースとしては、クロージャでのコールバックで受けたい
    • もし可能ならば RxSwift.Single に寄せたい

そこで、

コンパイルを通しつつ、さらに既存の設計の不満を解決するため、小さなラッパーを書いてみました。

やること

ジェネレートされたコード( {PREFIX}{API名}Client )のinitを書き替える

そもそもビルド通らないので、以下のように置き換えます。

    init(configuration: AWSServiceConfiguration) {
        super.init()

        var urlString = "https://xxxxxxxxxx.execute-api.{region}.amazonaws.com/{stage名}"
        if urlString.hasSuffix("/") {
            urlString = String(urlString.dropLast())
        }
        self.configuration = AWSServiceConfiguration(
            region: configuration.regionType,
            endpoint: AWSEndpoint(region: configuration.regionType, service: .APIGateway, url: URL(string: urlString)),
            credentialsProvider: configuration.credentialsProvider
        )

        let signer = AWSSignatureV4Signer(credentialsProvider: self.configuration.credentialsProvider, endpoint: self.configuration.endpoint)!
        if let endpoint = self.configuration.endpoint {
            self.configuration.baseURL = endpoint.url
        }
        self.configuration.requestInterceptors = [AWSNetworkingRequestInterceptor(), signer]
    }

{PREFIX}{API名}Client のラッパーを用意する

/// API Gatewayの自動生成コードをラップする構造体
struct APIGateway {
    enum Method: String {
        case GET
        case POST
        case PUT
        case DELETE
        case HEAD
        case PATCH
        case OPTIONS
    }
    enum DecodingError: Swift.Error {
        case unknown
    }
    enum Result<T> {
        case success(T)
        case error(Swift.Error)
    }

    /// 作成したAPI/ステージなどの環境に応じて書き替える
    fileprivate typealias GeneratedClient = {PREFIX}{API名}Client
    private static let clientKey = "適当な文字列"

    /// didFinishLaunchingWithOptionsで呼ぶやつ
    static func register() {
        let credentialProvider = AWSCognitoCredentialsProvider(regionType: .APNortheast1, identityPoolId: "ap-northeast-1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
        let configuration = AWSServiceConfiguration(region: .APNortheast1, credentialsProvider: credentialProvider)!
        GeneratedClient.registerClient(withConfiguration: configuration, forKey: clientKey)
    }

    fileprivate static var generated: GeneratedClient {
        return GeneratedClient.client(forKey: clientKey)
    }
}

/// AWSAPIGatewayRequest, AWSTaskをラップしてSwiftyにAPI定義を記述するためのプロトコル
protocol APIGatewayRequest {
    associatedtype Response

    var httpMethod: APIGateway.Method { get }
    var urlString: String { get }
    var queryParameters: [AnyHashable: Any] { get }
    var headerParameters: [AnyHashable: Any] { get }
    var httpBody: Any? { get }
}
extension APIGatewayRequest {
    var headerParameters: [AnyHashable: Any] {
        return [
            "Content-Type": "application/json",
            "Accept": "application/json",
        ]
    }
    var queryParameters: [AnyHashable: Any] { return [:] }
    var httpBody: Any? { return nil }

    private var awsRequest: AWSAPIGatewayRequest {
        return AWSAPIGatewayRequest(
            httpMethod: httpMethod.rawValue,
            urlString: urlString,
            queryParameters: queryParameters,
            headerParameters: headerParameters,
            httpBody: httpBody
        )
    }
}
/// レスポンスが必要なエンドポイントに使う。Codableを活用する。
extension APIGatewayRequest where Response: Decodable {
    func invoke(completion: @escaping (APIGateway.Result<Response>) -> Void) {
        APIGateway.generated.invoke(awsRequest).continueWith(block: {
            if let data = $0.result?.responseData {
                do {
                    let t = try JSONDecoder().decode(Response.self, from: data)
                    completion(.success(t))
                } catch(let e) {
                    completion(.error(e))
                }
            } else {
                completion(.error($0.error ?? APIGateway.DecodingError.unknown))
            }
            return nil
        })
    }
}
/// レスポンスが不要なエンドポイントに使う
extension APIGatewayRequest where Response == Void {
    func invoke(completion: @escaping (APIGateway.Result<Void>) -> Void) {
        APIGateway.generated.invoke(awsRequest).continueWith(block: {
            if $0.result?.responseData != nil {
                completion(.success(Void()))
            } else {
                completion(.error($0.error ?? APIGateway.DecodingError.unknown))
            }
            return nil
        })
    }
}

// MARK: - RxSwift.Singleに寄せたい場合は以下を使う

import RxSwift
extension Single {
    static func from<T: APIGatewayRequest>(_ request: T) -> Single<Element> where T.Response == Element, T.Response: Decodable {
        return Single<Element>.create { emitter in
            request.invoke {
                switch $0 {
                case .success(let t): emitter(.success(t))
                case .error(let t): emitter(.error(t))
                }
            }
            return Disposables.create()
        }
    }
    static func from<T: APIGatewayRequest>(_ request: T) -> Single<Element> where T.Response == Element, T.Response == Void {
        return Single<Element>.create { emitter in
            request.invoke {
                switch $0 {
                case .success(let t): emitter(.success(t))
                case .error(let t): emitter(.error(t))
                }
            }
            return Disposables.create()
        }
    }
}

AppDelegateでregisterする

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        APIGateway.register()

        return true
    }

レスポンスの型はpure Swift な structで定義する

/// Pure Swift struct brought as the response from APIGateway.
struct Hoge: Codable {
    let id: Int
    let userID: Int
    let url: URL

    private enum CodingKeys: String, CodingKey {
        case id
        case userID = "user_id"
        case url
    }
}

APIGatewayRequest プロトコルに適合したエンドポイント定義を書いて、使う

例① URLパラメータを渡し、Codableな型を受け取る

作りたいもの

2017-11-04 11.09.10.png

エンドポイント定義

struct HogeGetRequest: APIGatewayRequest {
    typealias Response = [Hoge]

    let httpMethod: APIGateway.Method = .GET
    let urlString = "/hoges"
    let queryParameters: [AnyHashable: Any]

    init(userID: Int) {
        queryParameters = [
            "user_id": userID
        ]
    }
}

呼び出し方

        // 通信後の処理をコールバックで記述
        HogeGetRequest(userID: 1).invoke {
            switch $0 {
            case .success(let hoges): print(hoges)
            case .error(let error): print(error)
            }
        }
        // RxSwiftを使っているならこうも書ける
        Single.from(HogeGetRequest(userID: 1))
            .subscribe(onSuccess: { hoges in
                print(hoges)
            })
            .disposed(by: disposeBag)

例② application/json をbodyで渡し、200レスポンスが返ってくればそれでいい

作りたいもの

2017-11-04 11.07.04.png

エンドポイント定義

struct HogePostRequest: APIGatewayRequest {
    typealias Response = Void

    let httpMethod: APIGateway.Method = .POST
    let urlString = "/hoges"
    let httpBody: Any?

    struct Parameters: Encodable {
        let url: URL
    }
    init(parameters: Parameters) {
        httpBody = try! JSONEncoder().encode(parameters)
    }
}

呼び出し方

        // 通信後の処理をコールバックで記述
        HogePostRequest(parameters: .init(
            url: URL(string: "https://aws.amazon.com")!
            ))
            .invoke {
                switch $0 {
                case .success: () // Void
                case .error(let error): print(error)
                }
        }
        // RxSwiftを使っているならこうも書ける
        Single.from(HogePostRequest(parameters: .init(
            url: URL(string: "https://aws.amazon.com")!
            )))
            .subscribe()
            .disposed(by: disposeBag)

後始末

メソッドを置き換える際、 {PREFIX}{API名}Client にジェネレートされていたAPIアクセス用のメソッドと、そこで使われている AWSModel を継承した型は、不要なので消してしまいましょう。
これでスッキリしました。

というか

AWSのジェネレートするコードが良くなってくれればワークアラウンドがなくても済む話なので、進化を期待してます。
AWSの中の人の皆様方、よろしくお願いします :pray:

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2