Skip to content

Commit cde8e4a

Browse files
authored
Merge pull request #335 from tk0miya/cli
feat: Add command line interface for rbs_rails
2 parents 14a84e5 + 26f22f0 commit cde8e4a

File tree

17 files changed

+497
-76
lines changed

17 files changed

+497
-76
lines changed

Gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ gemspec
88
gem "rake", "~> 13.0"
99
gem 'rails', '>= 7.0'
1010
gem 'rbs', '>= 3'
11-
gem 'rbs-inline'
11+
gem 'rbs-inline', require: false
1212
gem 'steep', '>= 1.4'
1313
gem 'minitest'

README.md

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,23 @@ Run the following command. It generates `lib/tasks/rbs.rake`.
2626
$ bin/rails g rbs_rails:install
2727
```
2828

29-
Then, the following four tasks are available.
29+
Then, the following three tasks are available.
3030

31-
* `rbs_rails:prepare`: Install inspector modules for Active Record models. This task is required to run before loading Rails application.
3231
* `rbs_rails:generate_rbs_for_models`: Generate RBS files for Active Record models
3332
* `rbs_rails:generate_rbs_for_path_helpers`: Generate RBS files for path helpers
3433
* `rbs_rails:all`: Execute all tasks of RBS Rails
3534

36-
37-
If you invoke multiple tasks, please run `rbs_rails:prepare` first.
35+
You can also run rbs_rails from command line:
3836

3937
```console
40-
$ bin/rails rbs_rails:prepare some_task another_task rbs_rails:generate_rbs_for_models
38+
# Generate all RBS files
39+
$ bundle exec rbs_rails all
40+
41+
# Generate RBS files for models
42+
$ bundle exec rbs_rails models
43+
44+
# Generate RBS files for path helpers
45+
$ bundle exec rbs_rails path_helpers
4146
```
4247

4348
### Install RBS for `rails` gem
@@ -52,6 +57,31 @@ You need to install `rails` gem's RBS files. I highly recommend using `rbs colle
5257
$ bundle exec rbs collection install
5358
```
5459

60+
### Configuration
61+
62+
You can customize the behavior of rbs_rails via configuration file. Place one of the following files in your project:
63+
64+
* `.rbs_rails.rb` (in the project root)
65+
* `config/rbs_rails.rb`
66+
67+
```ruby
68+
RbsRails.configure do |config|
69+
# Specify the directory where RBS signatures will be generated
70+
# Default: Rails.root.join("sig/rbs_rails")
71+
config.signature_root_dir = "sig/rbs_rails"
72+
73+
# Define which models should be ignored during generation
74+
config.ignore_model_if do |klass|
75+
# Example: Ignore test models
76+
klass.name.start_with?("Test") ||
77+
# Example: Ignore models in specific namespaces
78+
klass.name.start_with?("Admin::") ||
79+
# Example: Ignore models without database tables
80+
!klass.table_exists?
81+
end
82+
end
83+
```
84+
5585
### Steep integration
5686

5787
Put the following code as `Steepfile`.

example/rbs_rails.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Example configuration file for rbs_rails
2+
#
3+
# Place this file in the root of your Rails project or in config/rbs_rails.rb
4+
5+
RbsRails.configure do |config|
6+
# Specify the directory where RBS signatures will be generated
7+
# Default: Rails.root.join("sig/rbs_rails")
8+
config.signature_root_dir = "sig/rbs_rails"
9+
10+
# Define a proc to determine which models should be ignored during generation
11+
# The proc receives a model class and should return true if the model should be ignored
12+
config.ignore_model_if do |klass|
13+
# Example: Ignore test models
14+
klass.name.start_with?("Test") ||
15+
# Example: Ignore models in the Admin namespace
16+
klass.name.start_with?("Admin::") ||
17+
# Example: Ignore models that are not backed by a database table
18+
!klass.table_exists? ||
19+
# Example: Ignore anonymous classes
20+
klass.name.blank?
21+
end
22+
end

exe/rbs_rails

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env ruby
2+
3+
require "rbs_rails"
4+
require "rbs_rails/cli"
5+
6+
exit RbsRails::CLI.new().run(ARGV)

lib/generators/rbs_rails/install_generator.rb

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,9 @@ def create_raketask #: void
88
require 'rbs_rails/rake_task'
99
1010
RbsRails::RakeTask.new do |task|
11-
# If you want to avoid generating RBS for some classes, comment in it.
12-
# default: nil
13-
#
14-
# task.ignore_model_if = -> (klass) { klass == MyClass }
15-
1611
# If you want to change the rake task namespace, comment in it.
1712
# default: :rbs_rails
1813
# task.name = :cool_rbs_rails
19-
20-
# If you want to change where RBS Rails writes RBSs into, comment in it.
21-
# default: Rails.root / 'sig/rbs_rails'
22-
# task.signature_root_dir = Rails.root / 'my_sig/rbs_rails'
2314
end
2415
rescue LoadError
2516
# failed to load rbs_rails. Skip to load rbs_rails tasks.

lib/rbs_rails/cli.rb

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
require "optparse"
2+
require "rbs_rails/cli/configuration"
3+
4+
module RbsRails
5+
# @rbs &block: (CLI::Configuration) -> void
6+
def self.configure(&block) #: void
7+
CLI::Configuration.configure(&block)
8+
end
9+
10+
class CLI
11+
attr_reader :config_file #: String?
12+
13+
# @rbs argv: Array[String]
14+
def run(argv) #: Integer
15+
parser = create_option_parser
16+
17+
begin
18+
args = parser.parse(argv)
19+
subcommand = args.shift || "help"
20+
21+
case subcommand
22+
when "help"
23+
$stdout.puts parser.help
24+
0
25+
when "version"
26+
$stdout.puts "rbs_rails #{RbsRails::VERSION}"
27+
0
28+
when "all"
29+
load_application
30+
load_config
31+
generate_models
32+
generate_path_helpers
33+
0
34+
when "models"
35+
load_application
36+
load_config
37+
generate_models
38+
0
39+
when "path_helpers"
40+
load_application
41+
load_config
42+
generate_path_helpers
43+
0
44+
else
45+
$stdout.puts "Unknown command: #{subcommand}"
46+
$stdout.puts parser.help
47+
1
48+
end
49+
rescue OptionParser::InvalidOption => e
50+
$stderr.puts "Error: #{e.message}"
51+
$stdout.puts parser.help
52+
1
53+
end
54+
rescue StandardError => e
55+
$stderr.puts "Error: #{e.message}"
56+
1
57+
end
58+
59+
private
60+
61+
def config #: Configuration
62+
Configuration.instance
63+
end
64+
65+
def load_config #: void
66+
if config_file
67+
load config_file
68+
else
69+
if File.exist?(".rbs_rails.rb")
70+
load ".rbs_rails.rb"
71+
elsif Rails.root.join("config/rbs_rails.rb").exist?
72+
load Rails.root.join("config/rbs_rails.rb").to_s
73+
end
74+
end
75+
end
76+
77+
def load_application #: void
78+
require_relative "#{Dir.getwd}/config/application"
79+
80+
install_hooks
81+
82+
Rails.application.initialize!
83+
rescue LoadError => e
84+
raise "Failed to load Rails application: #{e.message}"
85+
end
86+
87+
def install_hooks #: void
88+
# Load inspectors. This is necessary to load earlier than Rails application.
89+
require 'rbs_rails/active_record/enum'
90+
end
91+
92+
def generate_models #: void
93+
Rails.application.eager_load!
94+
95+
dep_builder = DependencyBuilder.new
96+
97+
::ActiveRecord::Base.descendants.each do |klass|
98+
generate_single_model(klass, dep_builder)
99+
rescue => e
100+
puts "Error generating RBS for #{klass.name} model"
101+
raise e
102+
end
103+
104+
if dep_rbs = dep_builder.build
105+
config.signature_root_dir.join('model_dependencies.rbs').write(dep_rbs)
106+
end
107+
end
108+
109+
# @rbs klass: singleton(ActiveRecord::Base)
110+
# @rbs dep_builder: DependencyBuilder
111+
def generate_single_model(klass, dep_builder) #: bool
112+
return false if config.ignored_model?(klass)
113+
return false unless RbsRails::ActiveRecord.generatable?(klass)
114+
115+
original_path, _line = Object.const_source_location(klass.name) rescue nil
116+
117+
rbs_relative_path = if original_path && Pathname.new(original_path).fnmatch?("#{Rails.root}/**")
118+
Pathname.new(original_path)
119+
.relative_path_from(Rails.root)
120+
.sub_ext('.rbs')
121+
else
122+
"app/models/#{klass.name.underscore}.rbs"
123+
end
124+
125+
path = config.signature_root_dir / rbs_relative_path
126+
path.dirname.mkpath
127+
128+
sig = RbsRails::ActiveRecord.class_to_rbs(klass, dependencies: dep_builder.deps)
129+
path.write sig
130+
dep_builder.done << klass.name
131+
132+
true
133+
end
134+
135+
def generate_path_helpers #: void
136+
path = config.signature_root_dir.join 'path_helpers.rbs'
137+
path.dirname.mkpath
138+
139+
sig = RbsRails::PathHelpers.generate
140+
path.write sig
141+
end
142+
143+
def create_option_parser #: OptionParser
144+
OptionParser.new do |opts|
145+
opts.banner = <<~BANNER
146+
Usage: rbs_rails [command] [options]
147+
148+
Commands:
149+
help Show this help message
150+
version Show version
151+
all Generate all RBS files
152+
models Generate RBS files for models
153+
path_helpers Generate RBS for Rails path helpers
154+
155+
Options:
156+
BANNER
157+
158+
opts.on("--signature-root-dir=DIR", "Specify the root directory for RBS signatures") do |dir|
159+
config.signature_root_dir = Pathname.new(dir)
160+
end
161+
162+
opts.on("--config=FILE", "Load configuration from FILE") do |file|
163+
@config_file = file
164+
end
165+
end
166+
end
167+
end
168+
end

lib/rbs_rails/cli/configuration.rb

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
require 'forwardable'
2+
require 'singleton'
3+
4+
module RbsRails
5+
class CLI
6+
class Configuration
7+
include Singleton
8+
9+
# @rbs!
10+
# def self.instance: () -> Configuration
11+
# def self.configure: () { (Configuration) -> void } -> void
12+
13+
class << self
14+
extend Forwardable
15+
16+
def_delegator :instance, :configure # steep:ignore
17+
end
18+
19+
# @rbs!
20+
# @signature_root_dir: Pathname?
21+
# @ignore_model_if: (^(singleton(ActiveRecord::Base)) -> bool)?
22+
23+
def initialize #: void
24+
@signature_root_dir = nil
25+
@ignore_model_if = nil
26+
end
27+
28+
# @rbs &block: (Configuration) -> void
29+
def configure(&block) #: void
30+
block.call(self)
31+
end
32+
33+
def signature_root_dir #: Pathname
34+
@signature_root_dir || Rails.root.join("sig/rbs_rails")
35+
end
36+
37+
# @rbs dir: String | Pathname
38+
def signature_root_dir=(dir) #: Pathname
39+
@signature_root_dir = case dir
40+
when String
41+
Pathname.new(dir)
42+
when Pathname
43+
dir
44+
else
45+
raise ArgumentError, "signature_root_dir must be String or Pathname"
46+
end
47+
end
48+
49+
# @rbs &block: (singleton(ActiveRecord::Base)) -> bool
50+
def ignore_model_if(&block) #: void
51+
@ignore_model_if = block
52+
end
53+
54+
# @rbs klass: singleton(ActiveRecord::Base)
55+
def ignored_model?(klass) #: bool
56+
ignore_model_if = @ignore_model_if
57+
return false unless ignore_model_if
58+
59+
ignore_model_if.call(klass)
60+
end
61+
end
62+
end
63+
end

0 commit comments

Comments
 (0)