Recently we released RBS v1.6 with rbs collection
feature which is "Bundler" for RBS! The feature is experimental now, and it will be GA in RBS v2 for Ruby 3.1. Now you can use it with gem install rbs
if you use Ruby 2.6+!
I'm an RBS maintainer and I designed and implemented the feature, so I'll describe rbs collection
feature in this article.
By the way, I made a presentation on Sep. 10 at RubyKaigi Takeout 2021 and explained rbs collection
in the talk. This article is based on the talk. See https://github.com/pocke/rubykaigi-2021 for more details (unfortunately I spoke in Japanese but it has English slides and subtitles).
What is RBS
First of all, I introduce RBS.
RBS is a language to describe Ruby types. It is similar to .d.ts
of TypeScript or header files of C-language.
The syntax is similar to Ruby.
The following code is a simple RBS example:
class Blog
attr_reader name: String
def initialize: (String name) -> void
def each_article: () { (Article) -> void } -> self # For blog.each_article { |article| ... }
| () -> Enumerator[Article, self] # For blog.each_article.map { |article| ... }
end
class Article
attr_reader title: String
attr_reader content: String
def initialize: (String title, String, content) -> void
def published_at: () -> Time
end
ruby/gem_rbs_collection
I need to describe another topic before describing rbs collection
. It is ruby/gem_rbs_collection GitHub repository.
The repository stores third-party gems' RBSs, like DefinitelyTyped.
rbs collection
feature integrates this repository and tools use RBS, such as rbs
command, Steep, and TypeProf.
rbs collection
It's the main topic of this article💪
rbs collection
manages third-party RBSs, for example, installing, updating, and loading.
The first section describes a motivating example of rbs collection
.
Before rbs collection
, managing third-party RBSs is not efficient.
The second section describes how rbs collection
solves the problem.
The last section describes known issues of rbs collection
.
The feature still has some known issues for now, and I'll solve the issues until RBS v2.
Motivating example
In this section, I describe how to set up third-party RBSs for a Rails app without rbs collection
as the motivating example.
How to set up
First, download ruby/gem_rbs_collection repository as a git submodule.
$ git submodule add https://github.com/ruby/gem_rbs_collection.git gem_rbs_collection
Then rbs
command is available but it needs many options. For example, the following command is necessary to validate RBSs.
# too many options ¯\_(ツ)_/¯
$ rbs -rlogger -rpathname -rmutex_m -rdate -rmonitor \
-rsingleton -rtsort -rtime -rrack \
-ractivesupport -ractionpack -ractivejob \
-ractivemodel -ractionview -ractiverecord \
-rrailties \
--repo=gem_rbs_collection validate
# TypeProf also needs the same options.
$ typeprof -rlogger -rpathname -rmutex_m -rdate -rmonitor \
-rsingleton -rtsort -rtime -rrack \
-ractivesupport -ractionpack -ractivejob \
-ractivemodel -ractionview -ractiverecord \
-rrailties \
--repo=gem_rbs_collection validate
-r
options specify library names you want to load, and --repo
option specifies the repository path which is the git submodule.
And Steep needs a different configuration to load the same RBSs.
# Steepfile
target :app do
# Specify a directory that contains target Ruby files to type check.
check 'app'
# Specify the git submoduled directory
repo_path "gem_rbs_collection"
# Specify the all library names to load.
library 'pathname', 'logger', 'mutex_m', 'date',
'monitor', 'singleton', 'tsort', 'time', 'rack',
'activesupport', 'actionpack', 'activejob',
'activemodel', 'actionview', 'activerecord',
'railties'
end
Many options are required because rbs, TypeProf, and Steep don't know which libraries are required.
Motivations
As you can see, managing third-party RBSs has problems. I explain three problems.
First, we need to resolve dependencies by hand. In the above examples, human resolved "my app depends on Active Support, then Active Support depends on Logger, and ...". It is too hard for humans 😂
Second, we need git submodule
or copying RBSs to download third-party RBSs. It's too primitive. If you want to use different revisions RBSs, you can't use submodule but copying RBSs looks a hell.
For example, you want to use gem X RBS with revision R, but want to use gem Y RBS with revision R', git submodule
doesn't work.
Third, we need to use different ways between RBS, Steep, and TypeProf to load RBSs. rbs
command and TypeProf need to specify library names as command-line options, but Steep needs to specify them in Steepfile
. It is not a single source so it is hard to maintain.
How rbs collection
solves the problem
rbs collection
automatically resolves dependencies, downloads RBSs, and loads RBSs from the single source. It means rbs collection
sovles these problems!
To use rbs collection
, you need to install rbs
gem v1.6+. Then initialize the configuration file with rbs collection init
command.
# Install rbs gem v1.6+
$ gem install rbs
# Initialize the config file
$ rbs collection init
warning: rbs collection is experimental, and the behavior may change until RBS v2.0
created: rbs_collection.yaml
After that, edit the generated configuration file, rbs_collection.yaml
. The full content is the following:
Note that I'll describe why you need to edit it in the "Known Issues" section.
# Download sources
sources:
- name: ruby/gem_rbs_collection
remote: https://github.com/ruby/gem_rbs_collection.git
revision: main
repo_dir: gems
# A directory to install the downloaded RBSs
path: .gem_rbs_collection
gems:
# Skip loading rbs gem's RBS.
# It's unnecessary if you don't use rbs as a library.
- name: rbs
ignore: true
# 👮👮👮 Add the following lines
- name: pathname
- name: logger
- name: mutex_m
- name: date
- name: monitor
- name: singleton
- name: tsort
- name: time
- name: set
Then, execute rbs collection install
command.
# rbs collection uses Gemfile.lock to resolve dependency,
# so execute `bundle install` before that to generate Gemfile.lock.
$ bundle install
# Install RBSs from ruby/gem_rbs_collection repo
$ rbs collection install
Finally, the CLI tools are available!
You can use rbs and TypeProf commands without options.
$ rbs validate
$ typeprof target.rb
You can use Steep with a few configurations.
# Steepfile
target :app do
# Specify a directory that contains target Ruby files to type check.
check 'app'
end
Note that the rbs collection
integrations with Steep and TypeProf have not been released yet now (2021-09-18). You need to install them from GitHub if you want to try them for now.
Steep integration was implemented in https://github.com/soutaro/steep/pull/420 but not yet released.
TypeProf integration was implemented in https://github.com/ruby/typeprof/pull/53 but not yet merged.
The mechanism of rbs collection install
rbs collection install
command works with the following steps.
- Generating
rbs_collection.lock.yaml
- Resolve the dependencies with Gemfile.lock.
- Write the gems to the lockfile,
rbs_collection.lock.yaml
.
- Installing RBSs
- Copy RBSs from ruby/gem_rbs_collection with the lockfile.
Then, rbs
, steep
and typeprof
commands load the installed RBSs.
Known Issues
Unfortunately rbs collection
has known issues.
Resolving Standard Libraries' Dependencies
rbs collection
doesn't resolve dependencies of most standard libraries. For example, logger
, pathname
, etc.
In many cases, standard libraries do not appear in Gemfile.lock
so rbs collection
can't find them from Gemfile.lock
.
We can do require
stdlibs even if the stdlib is not included in gemspec or Gemfile. So most gems don't have explicit dependencies to stdlibs. For example, rails
gem depends on many stdlibs such as pathname
, but the gemspec doesn't include pathname
.
I'll fix the problem by introducing a configuration file for RBS, such as one called manifest.yaml
. The file will contain all stdlib dependency names.
For example, if foo
gem depends on pathname
and logger
, the manifest.yaml
will be the following.
# ruby/gem_rbs_collection/gems/foo/1.0/manifest.yaml
depends_on:
- name: pathname
- name: logger
Other issues
rbs collection
has other known issues. See https://github.com/ruby/rbs/pull/589 and https://github.com/ruby/rbs/issues/794 for more information.
Conclusion
I introduced rbs collection
, which is the "Bundler" for RBS.
You can easily manage third party RBSs with the command.
I hope you will give it a try. Feel free to open issues if you have any problems!
Thank you for reading <3
Top comments (0)