Swift製webアプリケーションフレームワーク Vapor を読むやつ #2
前回テンプレートの configure まで読んだので boot 以降を読もうかと思いましたが、routesのところを読み損ねていたことに気づいた ので読みます。
/// 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があったときにぶっ壊れてたのをなおすためにいれたようです。
今日はここまで。
-
予想が外れるのもまたコードリーディングの楽しみではありますが↩