rbs_rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d9b730da55f64b177f506e6f29efa54bba6878e3cd36b49df76703ac749feb1a
4
+ data.tar.gz: c759ea33ab9071df4806b81f14b5ca125b2f2df04054c4f3a9d2eb6e305c92ab
5
+ SHA512:
6
+ metadata.gz: 7a8fbf409f920b17353906c93d0f86d3c94b270801ab184e2e92b47ceb050659dfaa163cd3d0204e54e970db325826ddb7c6a6df5e380f8342bae760c7d0025c
7
+ data.tar.gz: 06a2387300377054f6ccc1cd9a5414cb64753adfb9ef5e109f95234ec50e8cc5a83b148aa46cc9140cfd47705eb9275c7ca762824564993a6226a77351f33102
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in rbs_rails.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
data/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # RbsRails
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/rbs_rails`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'rbs_rails'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install rbs_rails
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/pocke/rbs_rails.
36
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,126 @@
1
+ class ActiveRecord::Base
2
+ def self.scope: (Symbol, ^(*untyped) -> untyped ) -> void
3
+ | (Symbol) { (*untyped) -> untyped } -> void
4
+ def self.belongs_to: (Symbol, ?untyped, **untyped) -> void
5
+ def self.has_many: (Symbol, ?untyped, **untyped) -> void
6
+ def self.has_one: (Symbol, ?untyped, **untyped) -> void
7
+ def self.transaction: [T] () { () -> T } -> T
8
+ def self.create!: (**untyped) -> instance
9
+ def self.validate: (*untyped) -> void
10
+ def self.validates: (*untyped) -> void
11
+ def self.enum: (Hash[Symbol, untyped]) -> void
12
+
13
+ # callbacks
14
+ def self.after_commit: (*untyped) -> void
15
+ def self.after_create: (*untyped) -> void
16
+ def self.after_destroy: (*untyped) -> void
17
+ def self.after_rollback: (*untyped) -> void
18
+ def self.after_save: (*untyped) -> void
19
+ def self.after_update: (*untyped) -> void
20
+ def self.after_validation: (*untyped) -> void
21
+ def self.around_create: (*untyped) -> void
22
+ def self.around_destroy: (*untyped) -> void
23
+ def self.around_save: (*untyped) -> void
24
+ def self.around_update: (*untyped) -> void
25
+ def self.before_create: (*untyped) -> void
26
+ def self.before_destroy: (*untyped) -> void
27
+ def self.before_save: (*untyped) -> void
28
+ def self.before_update: (*untyped) -> void
29
+ def self.before_validation: (*untyped) -> void
30
+
31
+ def will_save_change_to_attribute?: (String | Symbol attr_name, ?from: untyped, ?to: untyped) -> bool
32
+
33
+ def save!: () -> self
34
+ def save: () -> bool
35
+ def update!: (*untyped) -> self
36
+ def update: (*untyped) -> bool
37
+ def destroy!: () -> self
38
+ def destroy: () -> bool
39
+ def valid?: () -> bool
40
+ def invalid?: () -> bool
41
+ def errors: () -> untyped
42
+ def []: (Symbol) -> untyped
43
+ def []=: (Symbol, untyped) -> untyped
44
+ end
45
+
46
+ class ActiveRecord::Relation
47
+ end
48
+
49
+ class ActiveRecord::Associations::CollectionProxy
50
+ end
51
+
52
+ interface _ActiveRecord_Relation[Model]
53
+ def all: () -> self
54
+ def ids: () -> Array[Integer]
55
+ def none: () -> self
56
+ def pluck: (Symbol | String column) -> Array[untyped]
57
+ | (*Symbol | String columns) -> Array[Array[untyped]]
58
+ def where: (*untyped) -> self
59
+ def not: (*untyped) -> self
60
+ def exists?: (*untyped) -> bool
61
+ def order: (*untyped) -> self
62
+ def group: (*Symbol | String) -> untyped
63
+ def distinct: () -> self
64
+ def or: (self) -> self
65
+ def merge: (self) -> self
66
+ def joins: (*String | Symbol) -> self
67
+ | (Hash[untyped, untyped]) -> self
68
+ def left_joins: (*String | Symbol) -> self
69
+ | (Hash[untyped, untyped]) -> self
70
+ def left_outer_joins: (*String | Symbol) -> self
71
+ | (Hash[untyped, untyped]) -> self
72
+ def includes: (*String | Symbol) -> self
73
+ | (Hash[untyped, untyped]) -> self
74
+ def eager_load: (*String | Symbol) -> self
75
+ | (Hash[untyped, untyped]) -> self
76
+ def preload: (*String | Symbol) -> self
77
+ | (Hash[untyped, untyped]) -> self
78
+ def find_by: (*untyped) -> Model?
79
+ def find_by!: (*untyped) -> Model
80
+ def find: (Integer id) -> Model
81
+ def first: () -> Model
82
+ | (Integer count) -> Array[Model]
83
+ def find_each: (?batch_size: Integer, ?start: Integer, ?finish: Integer, ?error_on_ignore: bool) { (Model) -> void } -> nil
84
+ def find_in_batches: (?batch_size: Integer, ?start: Integer, ?finish: Integer, ?error_on_ignore: bool) { (self) -> void } -> nil
85
+ def destroy_all: () -> untyped
86
+ def delete_all: () -> untyped
87
+ def update_all: (*untyped) -> untyped
88
+ def each: () { (Model) -> void } -> self
89
+ end
90
+
91
+ interface _ActiveRecord_Relation_ClassMethods[Model, Relation]
92
+ def all: () -> Relation
93
+ def ids: () -> Array[Integer]
94
+ def none: () -> Relation
95
+ def pluck: (Symbol | String column) -> Array[untyped]
96
+ | (*Symbol | String columns) -> Array[Array[untyped]]
97
+ def where: (*untyped) -> Relation
98
+ def exists?: (*untyped) -> bool
99
+ def order: (*untyped) -> Relation
100
+ def group: (*Symbol | String) -> untyped
101
+ def distinct: () -> self
102
+ def or: (Relation) -> Relation
103
+ def merge: (Relation) -> Relation
104
+ def joins: (*String | Symbol) -> self
105
+ | (Hash[untyped, untyped]) -> self
106
+ def left_joins: (*String | Symbol) -> self
107
+ | (Hash[untyped, untyped]) -> self
108
+ def left_outer_joins: (*String | Symbol) -> self
109
+ | (Hash[untyped, untyped]) -> self
110
+ def includes: (*String | Symbol) -> self
111
+ | (Hash[untyped, untyped]) -> self
112
+ def eager_load: (*String | Symbol) -> self
113
+ | (Hash[untyped, untyped]) -> self
114
+ def preload: (*String | Symbol) -> self
115
+ | (Hash[untyped, untyped]) -> self
116
+ def find_by: (*untyped) -> Model?
117
+ def find_by!: (*untyped) -> Model
118
+ def find: (Integer id) -> Model
119
+ def first: () -> Model
120
+ | (Integer count) -> Array[Model]
121
+ def find_each: (?batch_size: Integer, ?start: Integer, ?finish: Integer, ?error_on_ignore: bool) { (Model) -> void } -> nil
122
+ def find_in_batches: (?batch_size: Integer, ?start: Integer, ?finish: Integer, ?error_on_ignore: bool) { (self) -> void } -> nil
123
+ def destroy_all: () -> untyped
124
+ def delete_all: () -> untyped
125
+ def update_all: (*untyped) -> untyped
126
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "rbs_rails"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/rbs_rails.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'parser/current'
2
+
3
+ require_relative "rbs_rails/version"
4
+ require_relative 'rbs_rails/active_record'
5
+ require_relative 'rbs_rails/path_helpers'
6
+
7
+ module RbsRails
8
+ class Error < StandardError; end
9
+
10
+ def self.copy_signatures(to:)
11
+ from = Pathname(__dir__) / '../assets/sig/'
12
+ to = Pathname(to)
13
+ FileUtils.cp_r(from, to)
14
+ end
15
+ end
@@ -0,0 +1,272 @@
1
+ module RbsRails
2
+ module ActiveRecord
3
+ def self.class_to_rbs(klass, mode:)
4
+ Generator.new(klass, mode: mode).generate
5
+ end
6
+
7
+ class Generator
8
+ def initialize(klass, mode:)
9
+ @klass = klass
10
+ @mode = mode
11
+ end
12
+
13
+ def generate
14
+ [
15
+ klass_decl,
16
+ relation_decl,
17
+ collection_proxy_decl,
18
+ ].join("\n")
19
+ end
20
+
21
+ private def klass_decl
22
+ <<~RBS
23
+ #{header}
24
+ extend _ActiveRecord_Relation_ClassMethods[#{klass.name}, #{relation_class_name}]
25
+
26
+ #{columns.indent(2)}
27
+ #{associations.indent(2)}
28
+ #{enum_instance_methods.indent(2)}
29
+ #{enum_scope_methods(singleton: true).indent(2)}
30
+ #{scopes(singleton: true).indent(2)}
31
+ end
32
+ RBS
33
+ end
34
+
35
+ private def relation_decl
36
+ <<~RBS
37
+ class #{relation_class_name} < ActiveRecord::Relation
38
+ include _ActiveRecord_Relation[#{klass.name}]
39
+ include Enumerable[#{klass.name}, self]
40
+ #{enum_scope_methods(singleton: false).indent(2)}
41
+ #{scopes(singleton: false).indent(2)}
42
+ end
43
+ RBS
44
+ end
45
+
46
+ private def collection_proxy_decl
47
+ <<~RBS
48
+ class #{klass.name}::ActiveRecord_Associations_CollectionProxy < ActiveRecord::Associations::CollectionProxy
49
+ end
50
+ RBS
51
+ end
52
+
53
+
54
+ private def header
55
+ case mode
56
+ when :extension
57
+ "extension #{klass.name} (RbsRails)"
58
+ when :class
59
+ "class #{klass.name} < #{klass.superclass.name}"
60
+ else
61
+ raise "unexpected mode: #{mode}"
62
+ end
63
+ end
64
+
65
+ private def associations
66
+ [
67
+ has_many,
68
+ has_one,
69
+ belongs_to,
70
+ ].join("\n")
71
+ end
72
+
73
+ private def has_many
74
+ klass.reflect_on_all_associations(:has_many).map do |a|
75
+ "def #{a.name}: () -> #{a.klass.name}::ActiveRecord_Associations_CollectionProxy"
76
+ end.join("\n")
77
+ end
78
+
79
+ private def has_one
80
+ klass.reflect_on_all_associations(:has_one).map do |a|
81
+ type = a.polymorphic? ? 'untyped' : a.klass.name
82
+ "def #{a.name}: () -> #{type}"
83
+ end.join("\n")
84
+ end
85
+
86
+ private def belongs_to
87
+ klass.reflect_on_all_associations(:belongs_to).map do |a|
88
+ type = a.polymorphic? ? 'untyped' : a.klass.name
89
+ "def #{a.name}: () -> #{type}"
90
+ end.join("\n")
91
+ end
92
+
93
+ private def enum_instance_methods
94
+ methods = []
95
+ enum_definitions.each do |hash|
96
+ hash.each do |name, values|
97
+ next if name == :_prefix || name == :_suffix
98
+
99
+ values.each do |label, value|
100
+ value_method_name = enum_method_name(hash, name, label)
101
+ methods << "def #{value_method_name}!: () -> bool"
102
+ methods << "def #{value_method_name}?: () -> bool"
103
+ end
104
+ end
105
+ end
106
+
107
+ methods.join("\n")
108
+ end
109
+
110
+ private def enum_scope_methods(singleton:)
111
+ methods = []
112
+ enum_definitions.each do |hash|
113
+ hash.each do |name, values|
114
+ next if name == :_prefix || name == :_suffix
115
+
116
+ values.each do |label, value|
117
+ value_method_name = enum_method_name(hash, name, label)
118
+ methods << "def #{singleton ? 'self.' : ''}#{value_method_name}: () -> #{relation_class_name}"
119
+ end
120
+ end
121
+ end
122
+ methods.join("\n")
123
+ end
124
+
125
+ private def enum_definitions
126
+ @enum_definitions ||= build_enum_definitions
127
+ end
128
+
129
+ # We need static analysis to detect enum.
130
+ # ActiveRecord has `defined_enums` method,
131
+ # but it does not contain _prefix and _suffix information.
132
+ private def build_enum_definitions
133
+ ast = parse_model_file
134
+ return [] unless ast
135
+
136
+ traverse(ast).map do |node|
137
+ next unless node.type == :send
138
+ next unless node.children[0].nil?
139
+ next unless node.children[1] == :enum
140
+
141
+ definitions = node.children[2]
142
+ next unless definitions
143
+ next unless definitions.type == :hash
144
+ next unless traverse(definitions).all? { |n| [:str, :sym, :int, :hash, :pair, :true, :false].include?(n.type) }
145
+
146
+ code = definitions.loc.expression.source
147
+ code = "{#{code}}" if code[0] != '{'
148
+ eval(code)
149
+ end.compact
150
+ end
151
+
152
+ private def enum_method_name(hash, name, label)
153
+ enum_prefix = hash[:_prefix]
154
+ enum_suffix = hash[:_suffix]
155
+
156
+ if enum_prefix == true
157
+ prefix = "#{name}_"
158
+ elsif enum_prefix
159
+ prefix = "#{enum_prefix}_"
160
+ end
161
+ if enum_suffix == true
162
+ suffix = "_#{name}"
163
+ elsif enum_suffix
164
+ suffix = "_#{enum_suffix}"
165
+ end
166
+
167
+ "#{prefix}#{label}#{suffix}"
168
+ end
169
+
170
+ private def scopes(singleton:)
171
+ ast = parse_model_file
172
+ return '' unless ast
173
+
174
+ traverse(ast).map do |node|
175
+ next unless node.type == :send
176
+ next unless node.children[0].nil?
177
+ next unless node.children[1] == :scope
178
+
179
+ name_node = node.children[2]
180
+ next unless name_node
181
+ next unless name_node.type == :sym
182
+
183
+ name = name_node.children[0]
184
+ body_node = node.children[3]
185
+ next unless body_node
186
+ next unless body_node.type == :block
187
+
188
+ args = args_to_type(body_node.children[1])
189
+ "def #{singleton ? 'self.' : ''}#{name}: (#{args}) -> #{relation_class_name}"
190
+ end.compact.join("\n")
191
+ end
192
+
193
+ private def args_to_type(args_node)
194
+ res = []
195
+ args_node.children.each do |node|
196
+ case node.type
197
+ when :arg
198
+ res << "untyped"
199
+ when :optarg
200
+ res << "?untyped"
201
+ when :kwarg
202
+ res << "#{node.children[0]}: untyped"
203
+ when :kwoptarg
204
+ res << "?#{node.children[0]}: untyped"
205
+ else
206
+ raise "unexpected: #{node}"
207
+ end
208
+ end
209
+ res.join(", ")
210
+ end
211
+
212
+ private def parse_model_file
213
+ return @parse_model_file if defined?(@parse_model_file)
214
+
215
+
216
+ path = Rails.root.join('app/models/', klass.name.underscore + '.rb')
217
+ return @parse_model_file = nil unless path.exist?
218
+ return [] unless path.exist?
219
+
220
+ ast = Parser::CurrentRuby.parse path.read
221
+ return @parse_model_file = nil unless path.exist?
222
+
223
+ @parse_model_file = ast
224
+ end
225
+
226
+ private def traverse(node, &block)
227
+ return to_enum(__method__, node) unless block_given?
228
+
229
+ block.call node
230
+ node.children.each do |child|
231
+ traverse(child, &block) if child.is_a?(Parser::AST::Node)
232
+ end
233
+ end
234
+
235
+ private def relation_class_name
236
+ "#{klass.name}::ActiveRecord_Relation"
237
+ end
238
+
239
+ private def columns
240
+ klass.columns.map do |col|
241
+ "attr_accessor #{col.name} (): #{sql_type_to_class(col.type)}"
242
+ end.join("\n")
243
+ end
244
+
245
+ private def sql_type_to_class(t)
246
+ case t
247
+ when :integer
248
+ Integer.name
249
+ when :string, :text, :uuid
250
+ String.name
251
+ when :datetime
252
+ # TODO
253
+ # ActiveSupport::TimeWithZone.name
254
+ Time.name
255
+ when :boolean
256
+ "TrueClass | FalseClass"
257
+ when :jsonb, :json
258
+ "untyped"
259
+ when :date
260
+ # TODO
261
+ # Date.name
262
+ 'untyped'
263
+ else
264
+ raise "unexpected: #{t.inspect}"
265
+ end
266
+ end
267
+
268
+ private
269
+ attr_reader :klass, :mode
270
+ end
271
+ end
272
+ end
@@ -0,0 +1,31 @@
1
+ module RbsRails
2
+ class PathHelpers
3
+ def self.generate(routes: Rails.application.routes)
4
+ new(routes: Rails.application.routes).generate
5
+ end
6
+
7
+ def initialize(routes:)
8
+ @routes = routes
9
+ end
10
+
11
+ def generate
12
+ methods = helpers.map do |helper|
13
+ # TODO: More restrict argument types
14
+ "def #{helper}: (*untyped) -> String"
15
+ end
16
+
17
+ <<~RBS
18
+ interface _RbsRailsPathHelpers
19
+ #{methods.join("\n").indent(2)}
20
+ end
21
+ RBS
22
+ end
23
+
24
+ private def helpers
25
+ routes.named_routes.helper_names
26
+ end
27
+
28
+ private
29
+ attr_reader :routes
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ module RbsRails
2
+ VERSION = "0.1.0"
3
+ end
data/rbs_rails.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ require_relative 'lib/rbs_rails/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "rbs_rails"
5
+ spec.version = RbsRails::VERSION
6
+ spec.authors = ["Masataka Pocke Kuwabara"]
7
+ spec.email = ["kuwabara@pocke.me"]
8
+
9
+ spec.summary = %q{A RBS files generator for Rails application}
10
+ spec.description = %q{A RBS files generator for Rails application}
11
+ spec.homepage = "https://github.com/pocke/rbs_rails"
12
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
13
+
14
+ spec.metadata["homepage_uri"] = spec.homepage
15
+ spec.metadata["source_code_uri"] = spec.homepage
16
+ # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
17
+
18
+ # Specify which files should be added to the gem when it is released.
19
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
21
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
+ end
23
+ spec.bindir = "exe"
24
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
+ spec.require_paths = ["lib"]
26
+
27
+ spec.add_runtime_dependency 'parser'
28
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rbs_rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Masataka Pocke Kuwabara
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-02-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: parser
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: A RBS files generator for Rails application
28
+ email:
29
+ - kuwabara@pocke.me
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".gitignore"
35
+ - Gemfile
36
+ - README.md
37
+ - Rakefile
38
+ - assets/sig/active_record.rbs
39
+ - bin/console
40
+ - bin/setup
41
+ - lib/rbs_rails.rb
42
+ - lib/rbs_rails/active_record.rb
43
+ - lib/rbs_rails/path_helpers.rb
44
+ - lib/rbs_rails/version.rb
45
+ - rbs_rails.gemspec
46
+ homepage: https://github.com/pocke/rbs_rails
47
+ licenses: []
48
+ metadata:
49
+ homepage_uri: https://github.com/pocke/rbs_rails
50
+ source_code_uri: https://github.com/pocke/rbs_rails
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 2.3.0
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubygems_version: 3.2.0.pre1
67
+ signing_key:
68
+ specification_version: 4
69
+ summary: A RBS files generator for Rails application
70
+ test_files: []