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

hkdnet.hatenablog.com

boot 以降にいくか、と思っていたのですが boot 以降よりも middleware まわりのほうがよさそうなのでそちらを読むことにします。

さて、 middleware ですが、とりあえず既存の middleware を読めばいいでしょう。 ErrorMiddleware を読みます。

vapor/ErrorMiddleware.swift at 3.0.2 · vapor/vapor · GitHub

superclass あるいは adopted protocols1 として Middleware, ServiceType を宣言しているようです。Swift は Vapor でしか読んでないので文法事項が普通にわからなくて困りますね。 すげー雑に見ると、 public static func makeService(for worker: Container) throws -> ErrorMiddleware でサービスとしての ErrorMiddleware を返しているようです。その実態は public func respond(to req: Request, chainingTo next: Responder) throws -> Future<Response> として実装されているように見えますね。つまり以下のようになってればよさそうです。

  • スタティックメソッド makeService でサービスをつくる
  • インスタンスメソッド responsd で実リクエストを捌く

では早速適当に middleware をつくりましょう。特定のヘッダがあったときに特定のヘッダを返す、という実リクエスト/レスポンスにあんまり影響を与えないような何かにしておきます。クラス名は適当に MyMiddleware とかにしておきましょう。

/// Sources/App/mymiddleware.swift
import Vapor

public final class MyMiddleware: Middleware, ServiceType {
    /// See `ServiceType`.
    public static func makeService(for worker: Container) throws -> MyMiddleware {
        return try .default(environment: worker.environment, log: worker.make())
    }
    
    /// Create a default `MyMiddleware`.
    ///
    /// - parameters:
    ///     - environment: The environment to respect when presenting errors.
    ///     - log: Log destination.
    public static func `default`(environment: Environment, log: Logger) -> MyMiddleware {
        return .init()
    }

    public init() {
    }
    
    /// See `Middleware`.
    public func respond(to req: Request, chainingTo next: Responder) throws -> Future<Response> {
        return try next.respond(to: req).map { res in
            if req.http.headers.contains(name: "X-NYA-N") {
                res.http.headers.add(name: "X-NYA-N", value: "RESPONSE")
            }
            return res
        }
    }
}

respond 内では req.http.headers を見て分岐しています。X-NYA-N ヘッダがあれば X-NYA-N: RESPONSE というヘッダを返すようにしています。

これを早速使いましょう。 configure.swift の middlewares に追加すればよさそうです。

/// Sources/App/configure.swift
    /// 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
    middlewares.use(MyMiddleware.self) // 追加

しかしこれだけではエラーになってしまいます。

Run[81340:13566121] Fatal error: Error raised at top level: ⚠️ Service Error: No services are available for 'MyMiddleware'.
- id: ServiceError.make

対応するサービスがねーぞと言われてますね。うーん。DIする機構として Service というのがありそうなのはわかっているのですがいまいち把握できてません。とりあえず ErrorMiddleware は使えているので vapor/vapor 内で ErrorMiddleware に関して検索してみるとこんなんが出てきました。

vapor/Services+Default.swift at 3.0.2 · vapor/vapor · GitHub

services.register(ErrorMiddleware.self) とやってサービスとしても登録していますね。おそらくDIのほうでサービスクラスのリストをもっておいて、ミドルウェアが必要になったときに該当のサービスをリストから探索 → makeService を呼んでサービスのインスタンスを取得、としているのでしょう。MyMiddleware クラスもサービスとして登録してやります2

/// Sources/App/configure.swift
    /// 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
    middlewares.use(MyMiddleware.self)
    services.register(middlewares)
    services.register(MyMiddleware.self) // これを追加

これで無事立ち上がりました。レスポンスもいい感じです。

~/.g/g/h/HelloVapor ❯❯❯ curl -i localhost:8080/todos
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
content-length: 2
date: Sun, 13 May 2018 18:57:43 GMT

[]%
~/.g/g/h/HelloVapor ❯❯❯ curl -i -H "X-NYA-N: REQUEST" localhost:8080/todos
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
content-length: 2
X-NYA-N: RESPONSE
date: Sun, 13 May 2018 18:57:46 GMT

[]% 

service の lookup とかはたぶんこっちのレポジトリなんですがちゃんと追えてないです。また次回かな。

github.com


  1. これまとめてなんていうんだろ。 ref: https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Declarations.html#//apple_ref/swift/grammar/class-declaration

  2. これミドルウェアに登録したときに該当する Service がなかったら asyncRun よりも前で落ちてほしいんだけどなあ