安全サーバーサイド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の別枠で改めて記事にするのでぜひ購読しておいてほしい。
現状できているもの
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_AskController
の get
というメソッドに流すように登録しているが、生成されたコードにはそのようなメソッドは無い。
これがこの仕組みの最も気に入っているところで、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枠で
の話を書く。お楽しみに。