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

hkdnet.hatenablog.com

前回テンプレートの configure まで読んだので boot 以降を読もうかと思いましたが、routesのところを読み損ねていたことに気づいた ので読みます。

https://github.com/vapor/api-template/blob/3df9b5518b510f5959c75ad04c1b51996a75e367/Sources/App/configure.swift#L11

/// Sources/App/routes.swift
import Vapor

/// Register your application's routes here.
public func routes(_ router: Router) throws {
    // Basic "Hello, world!" example
    router.get("hello") { req in
        return "Hello, world!"
    }

    // Example of configuring a controller
    let todoController = TodoController()
    router.get("todos", use: todoController.index)
    router.post("todos", use: todoController.create)
    router.delete("todos", Todo.parameter, use: todoController.delete)
}

まあなんとなく各 path + method の組み合わせについて関数的ななにか(型的には closure: @escaping (Request) throws -> T where T: ResponseEncodable っぽい)を渡していけばいいんだな、と予想ができます。気になるのは Todo.parameter ですね。Todo はモデルのクラスです。Todo.parameter は何を返すのでしょうか。まずは router.deleteシグネチャから確認しましょう。

    @discardableResult
    public func delete<T>(_ path: PathComponentsRepresentable..., use closure: @escaping (Request) throws -> T) -> Route<Responder>
        where T: ResponseEncodable
    {
        return _on(.DELETE, at: path.convertToPathComponents(), use: closure)
    }

path が可変長引数ですね。なので ["todos", Todo.parameter] が path であることがわかります。

convertToPathComponents() の実態をさぐりにいくと / 区切りで flatmap してそれぞれを path の一部として見ていることがわかります。

extension String: PathComponentsRepresentable {
    /// See `PathComponentsRepresentable`.
    public func convertToPathComponents() -> [PathComponent] {
        return split(separator: "/").map { .constant(.init($0)) }
    }
}

extension Array: PathComponentsRepresentable where Element == PathComponentsRepresentable {
    /// Converts self to an array of `PathComponent`.
    public func convertToPathComponents() -> [PathComponent] {
        return flatMap { $0.convertToPathComponents() }
    }
}

だからたぶん router.delete("foo", "bar")router.delete("foo/bar") は同じ。 翻りまして DELETE の形式はおそらく DELETE /todos/:id なので Todo.parameter:id に相当する何かであろうという予想が立ちます。まあ :id のところはただのプレースホルダーというか名前なので実際に使うほうをみてみましょう。

    /// Deletes a parameterized `Todo`.
    func delete(_ req: Request) throws -> Future<HTTPStatus> {
        return try req.parameters.next(Todo.self).flatMap { todo in
            return todo.delete(on: req)
        }.transform(to: .ok)
    }

next...?

    /// Grabs the next parameter from the parameter bag.
    ///
    ///     let id = try req.parameters.next(Int.self)
    ///
    /// - note: the parameters _must_ be fetched in the order they appear in the path.
    ///
    /// For example GET /posts/:post_id/comments/:comment_id must be fetched in this order:
    ///
    ///     let post = try req.parameters.next(Post.self)
    ///     let comment = try req.parameters.next(Comment.self)
    ///
    public func next<P>(_ parameter: P.Type) throws -> P.ResolvedParameter
        where P: Parameter
    {
        return try request._parameters.next(P.self, on: request)
    }

あ、これ parameter の name でとらない形式のやつじゃん……1 request._parameters.next の実際の実装はこんな感じ。

    public mutating func next<P>(_ parameter: P.Type, on container: Container) throws -> P.ResolvedParameter
        where P: Parameter
    {
        guard values.count > 0 else {
            throw RoutingError(identifier: "next", reason: "Insufficient parameters.")
        }

        let current = values[0]
        guard current.slug == P.routingSlug else {
            throw RoutingError(identifier: "nextType", reason: "Invalid parameter type: \(P.routingSlug) != \(current.slug)")
        }

        let item = try P.resolveParameter(current.value, on: container)
        values = Array(values.dropFirst())
        return item
    }

なんかパラメータだけ取りたいんだけど、ってときは resolveParameter のIFを満たす型であればよさそうですね。String とかは Vapor 内で定義されていました。雑に検証すると以下。

    router.get("echo", String.parameter) { req in
        return try req.parameters.next(String.self)
    }
~/.g/g/h/HelloVapor ❯❯❯ curl localhost:8080/echo/foo
foo%
~/.g/g/h/HelloVapor ❯❯❯ curl localhost:8080/echo/foo-dayo-n
foo-dayo-n% 

ちなみに自分で /echo/:foo とか書いても Not Found です。これは String をかいても PathComponent 内では constant 扱いになるから。

    router.get("echo/:content") { req in
        return try req.parameters.next(String.self)
    }
~/.g/g/h/HelloVapor ❯❯❯ curl localhost:8080/echo/foo
Not found% 

routes に関しては 3.0.1 から .catchall とかが入ってるっぽいんですが、これは .anything の後ろにpathがあったときにぶっ壊れてたのをなおすためにいれたようです。

github.com

今日はここまで。


  1. 予想が外れるのもまたコードリーディングの楽しみではありますが