Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 33 additions & 11 deletions docs/collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

`rbs collection` sub command manages third party gems' RBS. In short, it is `bundler` for RBS.

## Quickstart

In a project that has a `Gemfile.lock`:

```console
$ rbs collection init
$ rbs collection install
```

`rbs collection` reads `Gemfile.lock`, so there is no list of dependencies to maintain. `rbs` and type checkers such as Steep load the installed RBS files automatically.

## Requirements

* `git(1)`
Expand All @@ -17,33 +28,44 @@ First, generate the configuration file, `rbs_collection.yaml`, with `rbs collect
$ rbs collection init
created: rbs_collection.yaml

rbs collection installs RBS files for the gems in your Gemfile.lock.
Gems in Gemfile.lock don't need to be listed anywhere.

Next steps:
$ echo "/.gem_rbs_collection/" >> .gitignore
$ rbs collection install # writes rbs_collection.lock.yaml; keep it in version control

$ cat rbs_collection.yaml
# rbs collection installs RBS files for the gems in your Gemfile.lock.
# Run `rbs collection install` to resolve them into rbs_collection.lock.yaml and install them.

# Download sources
sources:
- name: ruby/gem_rbs_collection
- type: git
name: ruby/gem_rbs_collection
remote: https://github.com/ruby/gem_rbs_collection.git
revision: main
repo_dir: gems

# You can specify local directories as sources also.
# - type: local
# path: path/to/your/local/repository

# A directory to install the downloaded RBSs
path: .gem_rbs_collection

# gems:
# # RBS for a library that doesn't appear in Gemfile.lock, such as a non-gem standard library.
# - name: socket
#
# # If you want to avoid installing rbs files for gems, you can specify them here.
# - name: GEM_NAME
# ignore: true
```

I also recommend updating `.gitignore`.

```console
$ echo /.gem_rbs_collection/ >> .gitignore
```

### Install dependencies

Then, install gems' RBS with `rbs collection install`! It copies RBS from [the gem RBS repository](https://github.com/ruby/gem_rbs_collection) to `.gem_rbs_collection/` directory by default.
I recommend to ignore `.gem_rbs_collection/` from version control system, such as Git.

```console
$ rbs collection install
Expand Down Expand Up @@ -86,9 +108,9 @@ sources:
path: .gem_rbs_collection

gems:
# If the Gemfile.lock doesn't contain csv gem but you use csv gem,
# you can write the gem name explicitly to install RBS of the gem.
- name: csv
# If the Gemfile.lock doesn't contain socket but you use it,
# you can write the library name explicitly to install RBS of the library.
- name: socket

# If the Gemfile.lock contains nokogiri gem but you don't want to use the RBS,
# you can ignore the gem.
Expand Down
62 changes: 54 additions & 8 deletions lib/rbs/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1089,21 +1089,37 @@ def run_collection(args, options)

case args[0]
when 'install', 'instal', 'insta', 'inst', 'ins', 'in', 'i'
unless params[:frozen]
Collection::Config.generate_lockfile(config_path: config_path, definition: Bundler.definition)
if params[:frozen]
unless lock_path.exist?
if config_path.exist?
stderr.puts "#{lock_path} not found. Run `rbs collection install` without `--frozen` to generate it."
else
stderr.puts "#{lock_path} not found. Run `rbs collection init` and `rbs collection install` to generate it."
end
return 1
end
else
return 1 unless collection_config_available?(config_path)
definition = bundler_definition or return 1
Collection::Config.generate_lockfile(config_path: config_path, definition: definition)
end
Collection::Installer.new(lockfile_path: lock_path, stdout: stdout).install_from_lockfile
when 'update', 'updat', 'upda', 'upd', 'up', 'u'
return 1 unless collection_config_available?(config_path)
definition = bundler_definition or return 1
# TODO: Be aware of argv to update only specified gem
Collection::Config.generate_lockfile(config_path: config_path, definition: Bundler.definition, with_lockfile: false)
Collection::Config.generate_lockfile(config_path: config_path, definition: definition, with_lockfile: false)
Collection::Installer.new(lockfile_path: lock_path, stdout: stdout).install_from_lockfile
when 'init'
if config_path.exist?
puts "#{config_path} already exists"
stderr.puts "#{config_path} already exists"
return 1
end

config_path.write(<<~'YAML')
# rbs collection installs RBS files for the gems in your Gemfile.lock.
# Run `rbs collection install` to resolve them into rbs_collection.lock.yaml and install them.

# Download sources
sources:
- type: git
Expand All @@ -1120,27 +1136,57 @@ def run_collection(args, options)
path: .gem_rbs_collection

# gems:
# # RBS for a library that doesn't appear in Gemfile.lock, such as a non-gem standard library.
# - name: socket
#
# # If you want to avoid installing rbs files for gems, you can specify them here.
# - name: GEM_NAME
# ignore: true
YAML
stdout.puts "created: #{config_path}"
stdout.puts <<~MESSAGE
created: #{config_path}

rbs collection installs RBS files for the gems in your Gemfile.lock.
Gems in Gemfile.lock don't need to be listed anywhere.

Next steps:
$ echo "/.gem_rbs_collection/" >> .gitignore
$ rbs collection install # writes rbs_collection.lock.yaml; keep it in version control
MESSAGE
when 'clean'
unless lock_path.exist?
puts "#{lock_path} should exist to clean"
stderr.puts "#{lock_path} should exist to clean"
return 1
end
Collection::Cleaner.new(lockfile_path: lock_path)
when 'help', 'hel', 'he', 'h'
puts opts.help
stdout.puts opts.help
else
puts opts.help
stdout.puts opts.help
return 1
end

0
end

def collection_config_available?(config_path)
return true if config_path.exist?

stderr.puts "#{config_path} not found. Run `rbs collection init` to generate it."
false
end

def bundler_definition
definition = Bundler.definition
return definition if definition.lockfile.file?

stderr.puts "#{definition.lockfile} not found. Run `bundle install` to generate it."
nil
rescue Bundler::GemfileNotFound
stderr.puts "Gemfile not found. `rbs collection` reads the gems to install from Gemfile.lock."
nil
end

def collection_options(args)
OptionParser.new do |opts|
opts.banner = <<~HELP
Expand Down
6 changes: 6 additions & 0 deletions sig/cli.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ module RBS

def run_collection: (Array[String], LibraryOptions) -> Integer

# Returns `true` if the collection config exists, otherwise prints a message to `stderr` and returns `false`
def collection_config_available?: (Pathname) -> bool

# Returns `nil` if `Gemfile` or `Gemfile.lock` is not found, after printing a message to `stderr`
def bundler_definition: () -> Bundler::Definition?

def run_annotate: (Array[String], top) -> Integer

def run_subtract: (Array[String], top) -> Integer
Expand Down
3 changes: 3 additions & 0 deletions sig/shims/bundler.rbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
module Bundler
class GemfileNotFound < StandardError
end

class LockfileParser
def initialize: (String) -> void

Expand Down
128 changes: 128 additions & 0 deletions test/rbs/cli_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1082,6 +1082,134 @@ def foo: () -> void
end
end

def test_collection_init
Dir.mktmpdir do |dir|
Dir.chdir(dir) do
with_cli do |cli|
assert_cli_success do
cli.run(%w(collection init))
end

config = Pathname(dir).join(RBS::Collection::Config::PATH).read
assert_match(/gems in your Gemfile\.lock/, config)
assert_match(/rbs collection install/, config)

yaml = YAML.load(config)
assert_equal ".gem_rbs_collection", yaml["path"]
assert_equal ["ruby/gem_rbs_collection"], yaml["sources"].map {|source| source["name"] }

assert_match(/created: .*rbs_collection\.yaml/, stdout.string)
assert_match(/gems in your Gemfile\.lock/, stdout.string)
assert_match(/\$ rbs collection install/, stdout.string)
end
end
end
end

def test_collection_install_without_config
Dir.mktmpdir do |dir|
Dir.chdir(dir) do
with_cli do |cli|
refute_cli_success do
cli.run(%w(collection install))
end

assert_match(/rbs_collection\.yaml not found/, stderr.string)
assert_match(/rbs collection init/, stderr.string)
end
end
end
end

def test_collection_update_without_config
Dir.mktmpdir do |dir|
Dir.chdir(dir) do
with_cli do |cli|
refute_cli_success do
cli.run(%w(collection update))
end

assert_match(/rbs_collection\.yaml not found/, stderr.string)
assert_match(/rbs collection init/, stderr.string)
end
end
end
end

def test_collection_install_without_gemfile
Dir.mktmpdir do |dir|
Dir.chdir(dir) do
Pathname(dir).join(RBS::Collection::Config::PATH).write(<<~YAML)
sources: []
path: .gem_rbs_collection
YAML

_stdout, stderr = run_rbs_collection("install", bundler: false) do |status|
refute_predicate status, :success?
end

assert_match(/Gemfile not found/, stderr)
end
end
end

def test_collection_install_without_gemfile_lock
Dir.mktmpdir do |dir|
Dir.chdir(dir) do
dir = Pathname(dir)
dir.join(RBS::Collection::Config::PATH).write(<<~YAML)
sources: []
path: .gem_rbs_collection
YAML
dir.join("Gemfile").write(<<~RUBY)
source "https://rubygems.org"
RUBY

_stdout, stderr = run_rbs_collection("install", bundler: false) do |status|
refute_predicate status, :success?
end

assert_match(/Gemfile\.lock not found/, stderr)
assert_match(/bundle install/, stderr)
end
end
end

def test_collection_install_frozen_without_lockfile
Dir.mktmpdir do |dir|
Dir.chdir(dir) do
Pathname(dir).join(RBS::Collection::Config::PATH).write(<<~YAML)
sources: []
path: .gem_rbs_collection
YAML

with_cli do |cli|
refute_cli_success do
cli.run(%w(collection install --frozen))
end

assert_match(/rbs_collection\.lock\.yaml not found/, stderr.string)
assert_match(/without `--frozen`/, stderr.string)
end
end
end
end

def test_collection_install_frozen_without_config_and_lockfile
Dir.mktmpdir do |dir|
Dir.chdir(dir) do
with_cli do |cli|
refute_cli_success do
cli.run(%w(collection install --frozen))
end

assert_match(/rbs_collection\.lock\.yaml not found/, stderr.string)
assert_match(/rbs collection init/, stderr.string)
end
end
end
end

def test_collection_install
omit_on_jruby! "`rbs collection install` runs `bundle install`, which builds native gem extensions that do not compile on JRuby"

Expand Down
Loading