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

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な型を受け取る
作りたいもの

エンドポイント定義
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レスポンスが返ってくればそれでいい
作りたいもの

エンドポイント定義
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の中の人の皆様方、よろしくお願いします