WEBrick 1.9.0 として RBS による型情報つきの gem をリリースできたので、そのメモです。
バージョン
動作確認に使ったバージョンはこのあたりでした。
- ruby 3.3.5
- rbs 3.6.1
- steep 1.8.3
- webrick 1.8.2 → 1.9.0
使い方
rbs_collection.yaml
がなければ rbs collection init
で作成するなどして用意しておいて、以下のように gems
に - name: webrick
を追加して、 rbs collection install
や rbs collection update
を実行すると、 rbs_collection.lock.yaml
が更新されて steep などで型情報が使えるようになりました。
gems:
- name: webrick
rbs_collection.lock.yaml
には以下のように source.type: rubygems
で追加されていました。
- name: webrick
version: 1.9.0
source:
type: rubygems
ffi 1.17.0 は https://github.com/ffi/ffi/issues/1107 の問題があるので、 以下のように name: ffi
に ignore: true
を設定したものを gems:
に追加しています。
gems:
- name: webrick
- name: ffi
ignore: true # https://github.com/ffi/ffi/issues/1107
現状の型情報
typeprof で生成された https://github.com/ruby/webrick/pull/115 やソースコードを参考にしつつ、 rbs prototype rb
で生成した *.rbs
を更新する方法で、 https://github.com/ruby/webrick/pull/151 として追加しました。
2週間ぐらいかけて一通りざっと対応しただけなので、 まだ https://github.com/ruby/webrick/pull/155 のように間違っている部分や 考慮不足で使いにくい部分もあると思うので、改善案などあれば pull request や issue を 作成してもらえると良さそうです。
型付け中にひっかかった部分
singleton(ClassName)
例外クラスの指定のようにクラスオブジェクト自体が引数になるときは、 singleton(ClassName)
のように singleton
を使う、 というのが知らないと難しそうだった。
def self.register: (Numeric seconds, singleton(Exception) exception) -> Integer
const_set で定義されている定数
webrick/httpstatus
const_set
されている定数は以下で補ったので、 VSCode に Steep 拡張機能を入れているときに WEBrick::HTTPStatus::RC_OK: 200
などがホバーで確認できたり、補完がきいたりして便利です。
require 'webrick'
puts WEBrick::HTTPStatus::constants.grep(/\ARC_/).map{"#{_1}: #{WEBrick::HTTPStatus.const_get(_1)}"}
puts WEBrick::HTTPStatus::CodeToError.each_value.map{"class #{_1.name.split(/::/).last} < #{_1.superclass.name.split(/::/).last}\nend"}
method alias chain されているメソッド
Non-overloading method definition of `parse` in `::WEBrick::HTTPRequest` cannot be duplicated(RBS::DuplicatedMethodDefinition)
は https.rbs
に重複定義があったので、 | ...
を追加して、
alias orig_parse parse
def parse: (?(TCPSocket | OpenSSL::SSL::SSLSocket)? socket) -> void
| ...
のように定義して回避しました。
orig_parse
に再定義前の型が保存されているわけではなく、 再定義した parse
と同じ型になってしまうようなので、 厳密には alias
ではなく def orig_parse: 元の型
にした方が良さそうでしたが、 直接使うメソッドではないと思って、そこはがんばらずに自動生成されたままにしました。
いろいろな body
webrick/httpresponse
は body
に悩んで、コメントの
# Body may be:
# * a String;
# * an IO-like object that responds to +#read+ and +#readpartial+;
# * a Proc-like object that responds to +#call+.
を参考にして、
interface _CallableBody
def call: (_Writer) -> void
end
attr_accessor body: String | _ReaderPartial | _CallableBody
にしました。
#read
は呼ばれていなかったので、 _Reader & _ReaderPartial
ではなく _ReaderPartial
だけにしました。
書き込みは write
のみだったので、 socket
の型は _Writer
にしました。
=
つきメソッド
=
つきのメソッドの返り値でちょっと悩んでしまいましたが、他の RBS ファイルを確認すると右辺の値の型をそのまま書くようだったので、そうしておきました。
singleton
set_redirect
は singleton
を使って Redirect 系の例外クラスならどれでも受け付けるように
def set_redirect: (singleton(WEBrick::HTTPStatus::Redirect) status, URI::Generic | String url) -> bot
にしました。
read_body
IO?
型が渡せなくなるらしいという話があったので、 IO socket
を IO? socket
に変更した方がいいかもしれないと思ったのですが、 返り値も String?
になってしまうので、とりあえずそのままにしました。 実用上問題があれば、ユースケースと一緒に pull request を作ってほしいです。
最初は void block
にしていたのですが void
は返り値以外で書ける位置が制限されているらしいので top
に変更しました。
def read_body: (IO socket, body_chunk_block block) -> String
| (nil socket, top block) -> nil
Servlet の config や options の問題
AbstractServlet
は @config
が HTTPServer
で FileHandler
は Hash[Symbol, untyped]
で困ったので、
class AbstractServlet
@server: HTTPServer
interface _Config
def []: (Symbol) -> untyped
end
@config: _Config
にしました。
@options
も AbstractServlet
は Array[untyped]
で FileHandler
は Hash[Symbol, untyped]
なので、 AbstractServlet
の方は untyped
にしました。
返り値の型
do_GET
などが AbstractServlet
は -> bot
で FileHandler
で -> void
にしたのは型エラーにはならなかったので、継承で返り値がこのように変わるのは大丈夫のようです。
Enumerable の型
rbs prototype rb
で自動生成されただけの cgi.rbs
に型エラーがあると思って確認すると、こんな感じで include Enumerable[untyped]
になっていないからでした。
% cat a.rb
class C
include Enumerable
def each
yield nil
end
end
% rbs prototype rb a.rb
class C
include Enumerable
def each: () { (untyped) -> untyped } -> untyped
end
rbs prototype rb
は対応していなくて、 typeprof
なら対応してそうでした。 rbs prototype
には rbs prototype runtime
もあるらしく、そちらなら対応しているらしいです。
位置引数でも &block でも受けとる場合の型
webrick/httpserver.rbs
で
def mount_proc(dir, proc=nil, &block)
proc ||= block
raise HTTPServerError, "must pass a proc or block" unless proc
mount(dir, HTTPServlet::ProcHandler.new(proc))
end
に対応する型として、
def mount_proc: (String dir, ?HTTPServlet::ProcHandler::_Callable proc) -> void
| (String dir, ?nil proc) { (HTTPRequest, HTTPResponse) -> void } -> void
としてみました。
型付け後からリリースまでにひっかかった部分
確認のしやすさの都合で、 bitclust
の型付け作業をしている作業ディレクトリで webrick の rbs も作成していました。
そこから ruby/webrick
にコピーして pull request を作成しました。
その前に手元で rake build
して rake install:local
して動作確認していました。
その結果、 manifest.yaml
は gemspec
と同じトップではなく sig/manifest.yaml
に置く必要があるとわかりました。
リリースされる gem に rbs ファイルを含めるには webrick.gemspec
で sig/**/*.rbs
などを s.files
に追加する必要がありました。
stringio
を manifest.yaml
に入れるとエラーになって困ったので調べてみると、 https://github.com/ruby/rbs/tree/master/core にあったので、 core
のライブラリは不要なのかも、と思って書かなかったらエラーが消えました。 require
が必要なのに stringio
が core
に入っているのは分類ミスのようなので、 そのうち移動するかもしれないようです。
直接のリリース権限はないので、他の標準添付から分離されたり、されつつある gem と同じように https://github.com/ruby/webrick/blob/master/.github/workflows/push_gem.yml が追加されて、タグの push でリリースができました。
最後の Create GitHub release
の GITHUB_TOKEN
の secrets の設定ミスがあったらしく、 タグを消して push しなおしたら、gem のリリースの方でリリース済みバージョンということでそこまで進まなかったので、 今回だけ gh release create v1.9.0 --verify-tag --generate-notes
は手元で実行しました。
そもそもの経緯
RBS による型付け作業を開始した bitclust が webrick にも依存していて、 webrick の使っている部分だけ型付けをしてエラーがでないようにしていました。
しばらくして bitclust の型付けである程度ノウハウがたまったので、 webrick の型がちゃんとついている方が良さそうと思ったので、 bitclust 自体の作業を中断して、 webrick の方を一気に対応しました。
最後に
webrick は production 環境では使うべきでない、というものなので、一般公開用のサーバーとして使うことはないと思いますが、 限定された環境でのサーバーやソースコードを読む対象としてはまだまだ使われることがあると思うので、 機会があれば webrick の rbs を有効利用してみてください。