Swift製webアプリケーションフレームワーク Vapor を読むやつ #1

Vapor とは SSS(Server Side Swift) のwebアプリケーションフレームワークです。

docs: https://docs.vapor.codes/3.0/

セットアップ

CLI ツールの使用を推奨しているので brew でいれておく。プロジェクトの作成は vapor new で。Swift 初心者過ぎてよくわかっていないが XCode で開きたい場合は vapor xcode すると必要なファイルができるっぽい。

$ brew install vapor/tap/vapor 
$ vapor new HelloVapor # HelloVapor ディレクトリができる
$ cd HelloVapor
$ vapor xcode

この時点でのフォルダはこんな感じ。最初から git 管理されているので git ls-files で。

$ git ls-files
.gitignore
Package.resolved
Package.swift
Public/.gitkeep
README.md
Sources/App/Controllers/.gitkeep
Sources/App/Controllers/TodoController.swift
Sources/App/Models/.gitkeep
Sources/App/Models/Todo.swift
Sources/App/app.swift
Sources/App/boot.swift
Sources/App/configure.swift
Sources/App/routes.swift
Sources/Run/main.swift
Tests/.gitkeep
Tests/AppTests/AppTests.swift
Tests/LinuxMain.swift
circle.yml
cloud.yml

vapor new 時に --template (あるいは --web, --auth もしくは --api ) を指定することで設定可能です。以下のようなオプションがあることから単純に git clone してきているのだということがなんとなく推測できます。

       --branch An optional branch to specify when cloning
          --tag An optional tag to specify when cloning

template を作るときはコミットは squash して1コミットにしておいたほうがよさそうですね。

XCode から run して動かしてみます ( https://docs.vapor.codes/3.0/getting-started/xcode/ )

以下適当に動作確認。

$ open http://localhost:8080/hello/
$ curl localhost:8080/todos/
[]
$ curl -XPOST -d "title=1" localhost:8080/todos/
{"id":1,"title":"1"}
$ curl localhost:8080/todos/
[{"id":1,"title":"1"}]
$ curl -XPOST -d '{"title": "2"}' localhost:8080/todos/
{"error":true,"reason":"Value of type 'String' required for key ''."}
$ curl -XPOST -d '{"title": "2"}' -H 'Content-Type: application/json' localhost:8080/todos/
{"id":1,"title":"2"}
$ curl localhost:8080/todos/
[{"id":1,"title":"1"},{"id":2,"title":"2"}]

とりあえずいわゆるふつーのAPIサーバが立ってますね。

テンプレートを読む

デフォルトでは api テンプレートなのでそれにします。

https://github.com/vapor/api-template

エントリポイントっぽいところから。

/// Sources/Run/main.swift
import App

try app(.detect()).run()

Swift 初心者的には .detect() がキモいのですが、これは implicit member expression という書き方のようです。今回の場合は Environment.detect() の省略記法。 https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Expressions.html#//apple_ref/doc/uid/TP40014097-CH32-ID394

app 関数の定義が Sources/App/app.swift にあります。

/// Sources/App/app.swift
import Vapor

/// Creates an instance of Application. This is called from main.swift in the run target.
public func app(_ env: Environment) throws -> Application {
    var config = Config.default()
    var env = env
    var services = Services.default()
    try configure(&config, &env, &services)
    let app = try Application(config: config, environment: env, services: services)
    try boot(app)
    return app
}

ここから概念として Config, Service(s) が存在すること、 configure, boot を経て Application のインスタンス app が初期化されて返っていることがわかります。

configure から順に見ていきます。

/// Sources/App/configure.swift
import FluentSQLite
import Vapor

/// Called before your application initializes.
public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws {
    /// Register providers first
    try services.register(FluentSQLiteProvider())

    /// Register routes to the router
    let router = EngineRouter.default()
    try routes(router)
    services.register(router, as: Router.self)

    /// Register middleware
    var middlewares = MiddlewareConfig() // Create _empty_ middleware config
    /// middlewares.use(FileMiddleware.self) // Serves files from `Public/` directory
    middlewares.use(ErrorMiddleware.self) // Catches errors and converts to HTTP response
    services.register(middlewares)

    // Configure a SQLite database
    let sqlite = try SQLiteDatabase(storage: .memory)

    /// Register the configured SQLite database to the database config.
    var databases = DatabasesConfig()
    databases.add(database: sqlite, as: .sqlite)
    services.register(databases)

    /// Configure migrations
    var migrations = MigrationConfig()
    migrations.add(model: Todo.self, database: .sqlite)
    services.register(migrations)

}

FluentSQLite が ORM ですね。Vapor の内製ライブラリです。middleware の作り方も気になるので後で見ておきましょう。
マイグレーションは明示的に migrations.add してますけど、これ増えたときどうするんですかね。毎回増やすのかな。アノテーションとかでしれっと書けてほしいような1

マイグレーション自体は起動時に勝手にやってくれるようです。

[ INFO ] Migrating 'sqlite' database (FluentProvider.swift:28)
[ INFO ] Preparing migration 'Todo' (MigrationContainer.swift:50)
[ INFO ] Migrations complete (FluentProvider.swift:32)
Running default command: ...

具体的には try Application の中でやっているっぽい。ソース的にはここ。 https://github.com/vapor/vapor/blob/master/Sources/Vapor/Application.swift#L126-L140

今日はここまで。次回boot以降を読みます。

ハマったとこ

sqlite3 の path 問題

sqlite3 の .file("path") で path はディレクトリを含むと困ることがある。sqlite3側は mkdir などはしてくれないのでこちらでしてやる必要があるが、xcode の debug build が動くのは普段のプロジェクトとは違う場所なので mkdir db; touch db/.gitkeep とかしても無駄。

レポジトリ眺めてたらあとでこういう issue を発見した。 https://github.com/vapor/fluent-sqlite/issues/5

カラム追加

一回マイグレーションが動いたあとにモデルに属性を追加しても add_column とかはしてくれない。まあそりゃそうだ。どうなってるのが理想的なんだろう。


なお本記事は以下のエントリに触発されて書きました。

https://fiveteesixone.lackland.io/2018/05/06/addicted-to-vapor/


  1. Swift 的には attribute