hiragram no blog

iOSとか

安全サーバーサイドSwift

この記事はVASILY Advent Calendar 2017の9日目の記事です。

目指すのは労働からの引退。

VASILY開発合宿で取り組んだ内容です。

何を作りたいのか

ビットコインの売買価格は取引所によって異なる。そこで、安い取引所で買って、すぐに高い取引所で売ることができれば、ビットコインそのものの価格変動に左右されずに利益を得ることが出来る。これがアービトラージ取引である。

今回は、複数の取引所のAPIを叩いて定期的に売買価格を取得するサーバーサイドアプリケーションと、その情報を表示するiOSアプリケーションを作った。

システム構成

サーバーサイド

  • 言語: Swift
  • Webフレームワーク: Vapor
  • インフラ: Vapor Cloud
  • バッチ処理の一部にRxSwift

iOSアプリ

  • Swift, RxSwift, APIKit

その他

サーバーサイドのAPIとモデル型はSwaggerのドキュメントで管理し、各エンドポイントに対応するコントローラとモデル定義はSwaggerのYAMLから自動生成する仕組みを作った。具体的にはSwaggerのYAMLドキュメントをパースしてテンプレート通りにコードを生成するジェネレータをPythonで作った。テンプレートはAppleのgybで書く。

「Swaggerドキュメントから自動生成できないAPI/モデル定義はそもそも設計が間違っているのではないか」という仮説が自分の中にあり、それを検証するためのこのプロジェクトとも言える。

このコードジェネレータについてはVASILY Advent Calendar 2017の別枠で改めて記事にするのでぜひ購読しておいてほしい。

現状できているもの

f:id:hiragram:20171208200603p:plain:h600

CoincheckとBitflyerの価格を取得して、1BTCを安い方で買って高い方で売ったときの差益を表示している。スクリーンショットは1ヶ月ほど前のものなので、BTCすごいですねという感じ。古いスクリーンショットを出してきたのは、今の価格で表示したら桁が増えた影響でレイアウトがブチ壊れたからである。

アプリ側は特に面白いことはしていないので、この記事ではサーバーサイドアプリケーションについて解説する。

Vaporアプリケーションの開発

Vaporはオフィシャルに提供されたCLIコマンドがあり、Xcodeのプロジェクトファイルの生成やVapor Cloudへのデプロイ、Vapor CloudのDBインスタンスの管理など、開発フローの中で面倒に感じることの多い様々なことをコマンド一発でできるので便利。公式ドキュメントが充実しているので、詳しくは割愛する。

コントローラの自動生成

先述の通り、クライアントアプリに提供する各エンドポイントのコントローラはジェネレータによって自動生成される。

paths:
  /board/ask:
    get:
      tags:
        - Price
      summary: Latest fetched price information for each exchanges
      description: ""
      operationId: getLatestPriceForEachExchanges
      parameters: []
      responses:
        "200":
          description: successful operation
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Ask"

このようにSwagger側で定義されたコントローラは、以下のように生成される。

// path: /board/ask
final class Board_AskController {
    typealias GetResponse = ArrayResponse<Ask>

    let drop: Droplet

    init(drop: Droplet) {
        self.drop = drop
    }
}

extension Droplet {
    func setupGeneratedRoutes() throws {
        do {
            let controller = Board_AskController.init(drop: self)
            get("/board/ask", handler: controller.get) // (※)
        }
    }
}

ここで生成されるのは、各エンドポイントごとに1つのコントローラクラスと、Dropletと呼ばれるVaporアプリケーションのコアにルーティングを登録するコードである。賢明なSwiftエンジニアの読者は気づいたかもしれないが、このままでは(※)の行でコンパイルエラーになる。ルーターに対して /board/ask へのリクエストを Board_AskControllerget というメソッドに流すように登録しているが、生成されたコードにはそのようなメソッドは無い。

これがこの仕組みの最も気に入っているところで、Swagger上で定義されたモデル型のレスポンスを返すメソッドをデベロッパーが適切に実装しないとサーバーサイドアプリケーションのコンパイルが通らないのである。すなわち、Swaggerで定義されたエンドポイントを適切に実装していないと、デプロイはおろかコンパイルすら通らないので、あとになって実装漏れが発覚するとか、Swaggerと違う型のオブジェクトが返されてアプリ側が困ることは無い。

コンパイルを通すために、Ask 型のオブジェクトを配列で返す get メソッドを実装する。

extension Board_AskController: GetRequestHandler {
    func get(request: Request) throws -> ArrayResponse<Ask> {
        let exchanges = try Exchange.makeQuery().all()

        let prices = try exchanges.map {
            try Ask.makeQuery().filter("exchange_id", $0.id).all().last
            }
            .flatMap { $0 }

        return ArrayResponse.init(element: prices)
    }
}

これで、「DBにある各取引所の最新の買値データを配列にして返す」というエンドポイントが実装された。Ask の配列以外を返すとコンパイルは通らないので、間違った型のデータを返してしまう心配も無くなった。

ここまでまとめ

  • Swagger駆動開発、今のところ良いです
  • サーバーもクライアントもSwaggerを神ドキュメントとして扱う風習ができれば、業務に投入したい仕組みである。

この先

VASILYのアドベントカレンダーを3枠もらっているので、あと2枠で

  • 各取引所のAPIを叩いてデータを集めるバッチをVapor Cloud上に構築する
  • SwaggerからSwiftのコードを生成するPython製ジェネレータ

の話を書く。お楽しみに。