Swift製webアプリケーションフレームワーク Vapor を読むやつ #3
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>
として実装されているように見えますね。つまり以下のようになってればよさそうです。
では早速適当に 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 とかはたぶんこっちのレポジトリなんですがちゃんと追えてないです。また次回かな。
-
これまとめてなんていうんだろ。 ref: https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Declarations.html#//apple_ref/swift/grammar/class-declaration↩
-
これミドルウェアに登録したときに該当する Service がなかったら asyncRun よりも前で落ちてほしいんだけどなあ↩
Re: Rubyのdefの後に何を書けるか調べる実験
最後のHashが印象的。以下引用。
# no error def ({}).foo puts "foo" end # 別インスタンスなのでメソッドは呼べない {}.foo #=> (NoMethodError)
引用終了。
さて、別インスタンスなので呼べないというが、まあ呼びたいじゃん?オブジェクト作ってるんだから ObjectSpace から引っ張ってくればええやん?
def show_hash(h) if h p h p (h.methods - ({}.methods)) else puts "nil" end end def search_object_with_method(name:) ObjectSpace.each_object.find { |e| e.methods.include?(name) } end def ({}).foo $a = self end h = search_object_with_method(name: :foo) show_hash(h)
試してみまして。
$ ruby foo.rb {} [:foo]
ok
ついでだから初期化されたやつも試してみまして。
def show_hash(h) if h p h p (h.methods - ({}.methods)) else puts "nil" end end def search_object_with_method(name:) ObjectSpace.each_object.find { |e| e.methods.include?(name) } end def ({}).foo $a = self end h = search_object_with_method(name: :foo) show_hash(h) # ここから追記 puts '-' * 20 def ({a: 1}).bar end h = search_object_with_method(name: :bar) show_hash(h)
まあ、出るっしょ。
$ ruby foo.rb {} [:foo] -------------------- nil
nil !?!?!?!?
え、初期化するかどうかで振る舞い違うの!?ヤバくない?って思ったらGCのせいでした。どこにもバインドしてないからしょうがないね。
GC.disable # これを追加 def show_hash(h) if h p h p (h.methods - ({}.methods)) else puts "nil" end end def search_object_with_method(name:) ObjectSpace.each_object.find { |e| e.methods.include?(name) } end def ({}).foo $a = self end h = search_object_with_method(name: :foo) show_hash(h) puts '-' * 20 def ({a: 1}).bar end h = search_object_with_method(name: :bar) show_hash(h)
$ ruby foo.rb {} [:foo] -------------------- {:a=>1} [:bar]
hanachin さんが調べてくれてたけど NODE_HASH
が載ってないですね。わざとなのかしら。
— 三つ編み不審者 (@hanachin_) 2018年5月10日
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があったときにぶっ壊れてたのをなおすためにいれたようです。
今日はここまで。
-
予想が外れるのもまたコードリーディングの楽しみではありますが↩
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/
-
Swift 的には attribute↩
macOS 10.13.3 -> 10.13.4 のアップデートをするとSEGVするやつ
概要
朝起きたら macOS のアプデがかかったらしくログインを要求された。
いつも通りユーザーを選択しパスワードをいれて FileVault の解除を待つと見たことない画面へ。
「アプデ適用に失敗しちゃったっぽいよてへぺろ とりあえず再起動する?」と聞かれたので再起動するが症状が改善しない。
TL;DR
- cmd + option + R を押しながら起動
- wifi を選択して接続、アプデ内容をDL
- OS再インストール
NOTE: この操作によりデータが消えたりはしてないです
試したこと
再起動
無限ループ
セーフモード + アプデ再適用
セーフモードで起動すると App Store には「OSアプデがあるよ」と出ている。ので適用。
症状改善せず。
リカバリモードで起動
cmd + R のやつ。アプデ内容が落ちてこなくて終わり。
mcd + option + R のやつ。上述の通りなんとかなった。
参考情報
症状としては下記エントリと同じ。
SEGVしてるログが出る。
最終的に "failed to write private file" 的なログが出てるのがわかる。
ご参考までに。
Python3 script を雑に heroku で動かせるようにする
手元で動かしてた python スクリプトを web サービス化したときの備忘録です。
なお python 歴はゼロから deep learning 作ったくらいです。
モチベーション
秘伝の python script があった。
手元で動かすのがめんどくなったのでどうにかしたかったが、某サーバー群で python 動かすのとかがダルかったので heroku とかに適当にホストしておきたくなった。
要件は、heroku で動くこと、python3 であること、軽く認証っぽい何かがあることくらい。
実装
misc/python_simple_server.py at master · hkdnet/misc · GitHub
だいたい書いてある通り。 do_GET
潰すの忘れてデプロイしたら普通にディレクトリ見えてビビったのでマジで注意。
検証してないけど bind アドレスを忘れるとかもありそうである1。
requirements.txt はレポジトリルートに置いといたら勝手に読んでくれた。助かる。
Procfile はこんな感じ。heroku は 2018-05-02 現在デフォでpython3なので特に気にしなくてよい。
web: python server.py
雑に検証した感じがこれ
$ curl localhost:8000 $ curl -XPOST -d "foo=bar" localhost:8000 $ curl -XPOST -H "Authorization: Bearer YOUR_TOKEN" -d "foo=bar" localhost:8000 # サーバー側のログ↓ 127.0.0.1 - - [03/May/2018 04:44:15] "GET / HTTP/1.1" 400 - 127.0.0.1 - - [03/May/2018 04:44:26] "POST / HTTP/1.1" 400 - 127.0.0.1 - - [03/May/2018 04:44:41] "POST / HTTP/1.1" 200 -
蛇足
virtualenv -p python3 .
で環境つくっておくpip install autopep8
とautopep8 -i foo.py
しとく
-
最初から
'0.0.0.0'
指定したからそれで動かないのか自信がないが↩