Skip to content

Commit 4daefc4

Browse files
committed
cli: Implement rbs_rails server command (prototype)
To make RBS generation faster to extract signatures from the Ruby on Rails application, this provides the rbs_rails server. It generates signatures on demand if the IDE lets the rbs_rails server know the changes to Ruby files. The rbs_rails server preloads the Rails application on start and reloads it when the source code is changed. It improves the performance of signature generation rather than running the rbs_rails command The server accepts a POST request with a JSON body containing the "path" property, which is the path of the Ruby file from the Rails root. ``` curl -X POST http://localhost:8080 -d '{"path": "/app/models/user.rb"}' ```
1 parent cde8e4a commit 4daefc4

File tree

14 files changed

+245
-0
lines changed

14 files changed

+245
-0
lines changed

Gemfile.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ PATH
33
specs:
44
rbs_rails (0.12.1)
55
parser
6+
rackup
67
rbs (>= 1)
8+
webrick
79

810
GEM
911
remote: https://rubygems.org/
@@ -222,6 +224,7 @@ GEM
222224
unicode-emoji (4.0.4)
223225
uri (1.0.3)
224226
useragent (0.16.11)
227+
webrick (1.9.1)
225228
websocket-driver (0.7.6)
226229
websocket-extensions (>= 0.1.0)
227230
websocket-extensions (0.1.5)

example/rbs_rails.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,9 @@
1919
# Example: Ignore anonymous classes
2020
klass.name.blank?
2121
end
22+
23+
# Set the host and port for the rbs_rails server
24+
# Default: host is "0.0.0.0", port is 8080
25+
config.host = "localhost"
26+
config.port = 12345
2227
end

lib/rbs_rails/cli.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require "optparse"
22
require "rbs_rails/cli/configuration"
3+
require "rbs_rails/cli/server"
34

45
module RbsRails
56
# @rbs &block: (CLI::Configuration) -> void
@@ -41,6 +42,11 @@ def run(argv) #: Integer
4142
load_config
4243
generate_path_helpers
4344
0
45+
when "server"
46+
load_application
47+
load_config
48+
run_server
49+
0
4450
else
4551
$stdout.puts "Unknown command: #{subcommand}"
4652
$stdout.puts parser.help
@@ -140,6 +146,10 @@ def generate_path_helpers #: void
140146
path.write sig
141147
end
142148

149+
def run_server #: void
150+
Server.new(config).start
151+
end
152+
143153
def create_option_parser #: OptionParser
144154
OptionParser.new do |opts|
145155
opts.banner = <<~BANNER

lib/rbs_rails/cli/configuration.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,14 @@ class << self
2020
# @signature_root_dir: Pathname?
2121
# @ignore_model_if: (^(singleton(ActiveRecord::Base)) -> bool)?
2222

23+
attr_accessor :host #: String
24+
attr_accessor :port #: Integer
25+
2326
def initialize #: void
2427
@signature_root_dir = nil
2528
@ignore_model_if = nil
29+
@host = "0.0.0.0"
30+
@port = 8080
2631
end
2732

2833
# @rbs &block: (Configuration) -> void

lib/rbs_rails/cli/server.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
require "rackup"
2+
require "webrick"
3+
4+
require_relative "server/handler"
5+
6+
module RbsRails
7+
class CLI
8+
class Server
9+
attr_reader :config #: CLI::Configuration
10+
11+
# @rbs config: CLI::Configuration
12+
def initialize(config) #: void
13+
@config = config
14+
end
15+
16+
def start
17+
# Set the Rails environment to development
18+
Rails.env = "development"
19+
enable_zeitwerk_reloading
20+
21+
puts "Starting rbs_rails server on #{config.host}:#{config.port}"
22+
rack_app = build_rack_app
23+
Rackup::Handler::WEBrick.run(rack_app, Host: config.host, Port: config.port)
24+
25+
# Signal.trap(:INT) { server.shutdown }
26+
27+
# server.start
28+
rescue Interrupt
29+
puts "Server stopped."
30+
end
31+
32+
private
33+
34+
def enable_zeitwerk_reloading #: void
35+
Rails.autoloaders.main.enable_reloading
36+
end
37+
38+
def build_rack_app
39+
handler = Server::Handler.new(config)
40+
Rack::Builder.new do
41+
# @type self: Rack::Builder
42+
use ActionDispatch::Reloader, Rails.application.reloader
43+
run handler
44+
end
45+
end
46+
end
47+
end
48+
end
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
require "webrick"
2+
3+
module RbsRails
4+
class CLI
5+
class Server
6+
class Handler
7+
attr_reader :config #: CLI::Configuration
8+
9+
# @rbs config: CLI::Configuration
10+
def initialize(config) #: void
11+
@config = config
12+
end
13+
14+
# @rbs env: Rack::env
15+
def call(env) #: Rack::response
16+
case env["REQUEST_METHOD"]
17+
when "POST"
18+
request = JSON.parse(env["rack.input"].read)
19+
do_POST(request["path"])
20+
else
21+
[405, { "Content-Type" => "text/plain" }, ["Method Not Allowed"]]
22+
end
23+
end
24+
25+
# @rbs path: String?
26+
def do_POST(path) #: Rack::response
27+
path = regulate_path(path)
28+
if path.nil?
29+
[404, { "Content-Type" => "text/plain" }, ["Not Found"]]
30+
elsif path.extname != ".rb"
31+
[403, { "Content-Type" => "text/plain" }, ["Forbidden"]]
32+
else
33+
class_name = classify(path)
34+
if class_name.nil? || class_name.constantize.nil?
35+
[404, { "Content-Type" => "text/plain" }, ["Not Found"]]
36+
else
37+
generate_signature(class_name)
38+
[201, { "Content-Type" => "text/plain" }, ["Created"]]
39+
end
40+
end
41+
end
42+
43+
private
44+
45+
# @rbs path: String?
46+
def regulate_path(path) #: Pathname?
47+
return nil unless path
48+
49+
pathname = Rails.root.join(path[1..]).realpath
50+
return nil unless pathname.to_s.start_with?(Rails.root.to_s + File::SEPARATOR)
51+
52+
pathname.relative_path_from(Rails.root)
53+
rescue Errno::ENOENT
54+
nil
55+
end
56+
57+
# @rbs path: Pathname
58+
def classify(path) #: String?
59+
# if the specified file is placed under autoload_paths...
60+
Rails.application.config.autoload_paths.each do |autoload_path|
61+
next unless path.to_s.start_with?(autoload_path.to_s + File::SEPARATOR)
62+
63+
relative_path = path.relative_path_from(autoload_path)
64+
return relative_path.to_s.chomp(".rb").classify.constantize
65+
end
66+
67+
# The file will be placed under app/*/ directories
68+
relative_path = path.to_s.sub(%r{^app/(.*?)/}, "")
69+
relative_path.chomp(".rb").classify
70+
rescue NameError
71+
nil
72+
end
73+
74+
# @rbs path: String
75+
# @rbs class_name: String
76+
def generate_signature(class_name) #: void
77+
klass = class_name.constantize
78+
return unless klass < ::ActiveRecord::Base
79+
return if config.ignored_model?(klass)
80+
return unless ::RbsRails::ActiveRecord.generatable?(klass)
81+
82+
original_path, _line = Object.const_source_location(klass.name) rescue nil
83+
84+
rbs_relative_path = if original_path && Pathname.new(original_path).fnmatch?("#{Rails.root}/**")
85+
Pathname.new(original_path)
86+
.relative_path_from(Rails.root)
87+
.sub_ext('.rbs')
88+
else
89+
"app/models/#{klass.name.underscore}.rbs"
90+
end
91+
92+
path = config.signature_root_dir / rbs_relative_path
93+
path.dirname.mkpath
94+
95+
# TODO: We need to resolve the dependencies problem.
96+
sig = RbsRails::ActiveRecord.class_to_rbs(klass, dependencies: [])
97+
path.write sig
98+
end
99+
end
100+
end
101+
end
102+
end

rbs_collection.lock.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,4 +349,8 @@ gems:
349349
version: '0'
350350
source:
351351
type: stdlib
352+
- name: webrick
353+
version: 1.9.1
354+
source:
355+
type: rubygems
352356
gemfile_lock_path: Gemfile.lock

rbs_collection.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ gems:
3333
- name: optparse
3434
- name: tsort
3535
- name: forwardable
36+
- name: webrick

rbs_rails.gemspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,6 @@ Gem::Specification.new do |spec|
2727

2828
spec.add_runtime_dependency 'parser'
2929
spec.add_runtime_dependency 'rbs', '>= 1'
30+
spec.add_runtime_dependency 'rackup'
31+
spec.add_runtime_dependency 'webrick'
3032
end

sig/rackup.rbs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module Rackup
2+
module Handler
3+
class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet
4+
def self.run: (untyped app, Hash[untyped, untyped] options) -> void
5+
end
6+
end
7+
end

0 commit comments

Comments
 (0)