Skip to content

Instantly share code, notes, and snippets.

@kitasuke
Last active December 22, 2016 08:39
Show Gist options
  • Save kitasuke/b1f71678c47ae54548302e4e597bba6f to your computer and use it in GitHub Desktop.
Save kitasuke/b1f71678c47ae54548302e4e597bba6f to your computer and use it in GitHub Desktop.

Protocol BuffersをAPIKitとKituraで試してみる

この記事では、Protocol BuffersをSwiftでどう使うかサンプルアプリを使って説明します。Protocol Buffersの詳細はこちらの記事を先に読むと大体のイメージが掴めると思います。個人的には Protocol Buffersのメリットは型安全に書けて高速な通信が実現する ことだと思います。

サンプルアプリ

今回のサンプルアプリは、クライアント側はAPIKit、サーバ側はKituraで 通信処理を実装します。サンプルアプリはGitHubで公開しているので、こちらからダウンロードして試してみてください。

また、URLSessionで実装したサンプルアプリもこちらに用意しました。URLSessionだけでも楽に実装できるので、こちらも参考にしてみてください。

APIKitとは

APIKitとは、リクエストもレスポンスも型が明確になっていて、Swiftらしい書き方が出来るライブラリ です。 少し古いですが、詳細は開発者のブログ記事を見てください。そこから特徴だけを抜粋してみました。

  • リクエストに渡すパラメーターは型によって明らかになっている。
  • レスポンスはモデルオブジェクトとして受け取れる。
  • 成功時にレスポンスを非オプショナルな値で受け取れる。
  • 失敗時にエラーを非オプショナルな値で受け取れる。

型が決まっているのでAPIKitとProtocol Buffersは 相性が良さそう です。

対応バージョン

APIKitはバージョン3.1.0に[Protocol Buffersへの簡易的対応]((https://github.com/ishkawa/APIKit/pull/214)が含まれています。

Kituraとは

Kituraとは IBMが開発しているSwift用のWeb APIフレームワーク です。サーバサイドSwiftは他にもフレームワークがありますが、触ったことがあるのがKituraだったのと、IBMからKituraでProtocol Buffersを使ってみるにはという記事があったので、そちらを参考にしてKituraを使ってみました。

環境

今回のサンプルアプリは以下の環境で作成しました。

Swift 3.0.1
Xcode 8.2.1
protoc 3.1
swift-protobuf 0.9.26
Kitura 1.3
APIKit 3.1.0

内容

サンプルアプリの機能は、ボタンを押すとトークン情報を取得するだけです。GETPOSTでそれぞれ取得できるように実装しています。通信処理がProtocol Buffersを使ってどう実装するかだけを説明したいので、その他の実装はかなり適当です。その辺りは無視してください。

クライアント

まずは、クライアントの実装から見ていきます。

モデル

こちらが.protoファイルから自動生成されたTokenのモデルです。Tokenだけでなく、GetTokenResponse, PostTokenRequest, PostTokenResponseも一緒に生成されています。関係ある箇所のみ下記に抜粋してます。

struct Token: SwiftProtobuf.Message, SwiftProtobuf.Proto3Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf.ProtoNameProviding {
  var accessToken: String = ""

  init() {}
}


///   GET - Response
struct GetTokenResponse: SwiftProtobuf.Message, SwiftProtobuf.Proto3Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf.ProtoNameProviding {
    var token: Token {
    get {...}
    set {...}
  }

  init() {}
}

///   POST - Request
struct PostTokenRequest: SwiftProtobuf.Message, SwiftProtobuf.Proto3Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf.ProtoNameProviding {
    var accessToken: String = ""

    init() {}
}

///   POST - Response
struct PostTokenResponse: SwiftProtobuf.Message, SwiftProtobuf.Proto3Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf.ProtoNameProviding {
    var token: Token {
    get {...}
    set {...}
  }

  init() {}
}

次は通信エラー用のNetworkErrorモデルです。今回はエラーコードのenumbadRequestだけ用意しました。enumを生成した場合には初期値がunknownになり、想定外の値用にUNRECOGNIZED(Int)も用意されます。

struct NetworkError: SwiftProtobuf.Message, SwiftProtobuf.Proto3Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf.ProtoNameProviding {
  enum Code: SwiftProtobuf.Enum {
    typealias RawValue = Int

    ///   Unknown
    case unknown // = 0
    ///   400
    case badRequest // = 400
    case UNRECOGNIZED(Int)

    init() {
      self = .unknown
    }
  }

  ///   Error code
  var code: NetworkError.Code = NetworkError.Code.unknown

  ///   Optional: Message
  var message: String = ""
}

リクエストプロトコル

ここからはAPIKitの使い方の説明です。Kituraで走らせるサーバのURLとProtocol Buffers用のパーサーをセットします。

// サンプルアプリ用のリクエストプロトコルの宣言
protocol ProtobufRequest: Request {}

// デフォルト実装
extension ProtobufRequest {
    // ローカルホストのURL
    var baseURL: URL {
        return URL(string: "http://localhost:8090")!
    }

    // HTTPレスポンスのパーサー
    var dataParser: DataParser {
        return ProtobufDataParser()
    }
}

GETメソッドのリクエスト型

APIKitはリクエスト型を作る際にレスポンスの型も指定します。

// GETでトークンを取得するリクエスト
struct GetToken: ProtobufRequest {
    // HTTPレスポンスの型
    typealias Response = GetTokenResponse

    // HTTPメソッド
    var method: HTTPMethod {
        return .get
    }

    // パス
    var path: String {
        return "/v1/token"
    }
}

POSTメソッドのリクエスト型

POSTの場合はGET用リクエストでセットした情報に加えて、HTTPボディをセットします。Data型でセットするのでserializeProtobuf()メソッドを使ってシリアライズしています。

// POSTでトークンを取得するリクエスト
struct PostToken: ProtobufRequest {
    // HTTPレスポンスの型
    typealias Response = PostTokenResponse

    // HTTPメソッド
    var method: HTTPMethod {
        return .post
    }

    // パス
    var path: String {
        return "/v1/token"
    }

    // HTTPボディへセットするバイナリ
    var bodyParameters: BodyParameters? {
        var data = PostTokenRequest()
        data.accessToken = token.accessToken

        return ProtobufBodyParameters(protobufObject: try! data.serializeProtobuf())
    }

    private let token: Token

    init(token: Token) {
        self.token = token
    }
}

レスポンス

ステータスコードが正常系でないときもエラー情報をprotobufのデータで返しているので、protobufの型にデシリアライズしています。

extension ProtobufRequest where Response: SwiftProtobuf.Message {
    // レスポンスデータの処理
    func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {
        guard let data = object as? Data else {
            throw ResponseError.unexpectedObject(object)
        }

        // Data型をprotobuf型にデシリアライズ
        return try Response(protobuf: data)
    }

    func intercept(object: Any, urlResponse: HTTPURLResponse) throws -> Any {
        // ステータスコードのエラー処理
        guard 200..<300 ~= urlResponse.statusCode else {
            guard let data = object as? Data else {
                throw ResponseError.unacceptableStatusCode(urlResponse.statusCode)
            }

            // エラーの場合もprotobufのバイナリが返ってくるのでNetworkError型にデシリアライズ
            let error = try NetworkError(protobuf: data)
            throw ResponseError.unexpectedObject(error)
        }
        return object
    }
}

通信処理

GETでトークンを取得する処理です

Session.send(GetToken()) { result in
    switch result {
    case .success(let response):
        print(response)
    case .failure(let error):
        print(error)
    }
}

POSTでトークンを取得する処理です

var token = Token()
token.accessToken = "old token"

Session.send(PostToken(token: token)) { result in
    switch result {
    case .success(let response):
        print(response)
    case .failure(let error):
        print(error)
    }
}

こちらもPOSTでトークン情報を取得する処理ですが、ボディにセットするデータが適切ではないためにエラーが返ってきます。

Session.send(PostToken(token: Token())) { result in
    switch result {
    case .success(let response):
        print(response)
    case .failure(let error):
        print(error)
    }
}

APIKitとProtocol Buffersを組み合わせれば、より一層型安全に処理を実装できます。アプリの動作もコンパイル時に保証できるようになります。モックを使ったテストも書きやすいです。

サーバ

ポート番号設置絵

次はサーバ側の実装です。最初にポート番号を設定します。

private let router = Router()

Kitura.addHTTPServer(onPort: 8090, with: router)

Kitura.run()

GETリクエストの処理

GETメソッドで/v1/tokenパスにリクエストが来た時の処理です。 レスポンス型のGetTokenResponseをシリアライズして送っています。

router.get("/v1/token") { request, response, next in
    var token = Token()
    token.accessToken = "my token"

    var data = GetTokenResponse()
    data.token = token

    response.send(data: try data.serializeProtobuf())

    next()
}

POSTリクエストの処理

POSTメソッドで/v1/tokenパスにリクエストが来た時の処理です。 まずはリクエスト型のPostTokenRequestをデシリアライズします。その中のaccessTokenが空の場合はNetworkErrorを送っていて、そうでなければレスポンス型のPostTokenResponseをシリアライズして送っています。

router.post("/v1/token") { request, response, next in
    var body = Data()

    guard let bytes = try? request.read(into: &body),
        let token = try? PostTokenRequest(protobuf: body) else {
        return
    }

    guard token.accessToken.characters.count > 0 else {
        var error = NetworkError()
        error.code = .badRequest
        error.message = "invalid access token"

        response.status(.badRequest).send(data: try error.serializeProtobuf())

        next()
        return
    }

    var newToken = Token()
    newToken.accessToken = "new token"

    var data = PostTokenResponse()
    data.token = newToken

    response.status(.OK).send(data: try data.serializeProtobuf())

    next()
}

動作確認

クライアント

このサンプルアプリはCarthageを使っているので下記のコマンドを実行してください。

$ cd Client
$ carthage update

サーバ

最後に、サーバをローカルで立ち上げます。下記のコマンドを実行してください。

$ cd Server
$ swift build
$ ./.buid/debug/Server

もしくは、下記のコマンドを実行した後にServer.xcodeprojファイルを開いて、Serverターゲットを実行してください。

$ cd Server
$ swift package generate-xcodeproj

実行結果

サンプルアプリを実際に動かしてみましょう。まずはサーバを先に起動しておく必要があります。手順は上記の通りです。

次にクライアントを起動します。起動するとボタンが3つ見えると思います。

左のボタンはGETでトークン情報を取得します。

GetTokenResponse(token:Token(accessToken:"my token"))

真ん中のボタンはPOSTでトークン情報を取得します。

PostTokenResponse(token:Token(accessToken:"new token"))

右のボタンはトークン情報を取得しようとしてエラーになります。

responseError(APIKit.ResponseError.unexpectedObject(NetworkError(code:.badRequest,message:"invalid access token")))

まとめ

こちらの記事で説明した通り、リクエストもレスポンスも型があるのでどんな値を持っているかが明確ですね。クライアントは特に、JSONからprotobufに移行してもそれほどコードに変更を加えなくても大丈夫です。より型安全に実装できて、通信も速くなるので楽しくなりますね。皆さんも是非使ってみてください。

補足

今回のサンプルアプリはダウンロードして上記の手順に従えば動きますが、自分で一から作る場合は、swift-protobufのインストールやドキュメントの生成などが必要になります。その手順はこちらのREADMEを参考にしてください。

参考資料

https://developer.ibm.com/swift/2016/09/30/protocol-buffers-with-kitura/
https://github.com/kitasuke/ProtobufExample
http://www.kitura.io/
https://github.com/IBM-Swift/Kitura
https://github.com/ishkawa/APIKit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment