rbs_rails 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/.github/workflows/release.yml +27 -0
- data/CHANGELOG.md +64 -0
- data/Gemfile +9 -3
- data/Gemfile.lock +202 -129
- data/README.md +47 -1
- data/Rakefile +7 -2
- data/Steepfile +5 -0
- data/example/rbs_rails.rb +27 -0
- data/exe/rbs_rails +6 -0
- data/lib/generators/rbs_rails/install_generator.rb +10 -15
- data/lib/rbs_rails/active_record/enum.rb +81 -0
- data/lib/rbs_rails/active_record.rb +269 -171
- data/lib/rbs_rails/cli/configuration.rb +66 -0
- data/lib/rbs_rails/cli.rb +173 -0
- data/lib/rbs_rails/dependency_builder.rb +29 -8
- data/lib/rbs_rails/path_helpers.rb +14 -2
- data/lib/rbs_rails/rake_task.rb +39 -41
- data/lib/rbs_rails/util/file_writer.rb +22 -0
- data/lib/rbs_rails/util.rb +17 -15
- data/lib/rbs_rails/version.rb +1 -1
- data/lib/rbs_rails.rb +5 -2
- data/rbs_collection.lock.yaml +273 -45
- data/rbs_collection.yaml +1 -18
- data/rbs_rails.gemspec +2 -1
- data/sig/{install_generator.rbs → generators/rbs_rails/install_generator.rbs} +2 -0
- data/sig/rbs_rails/active_record/enum.rbs +26 -0
- data/sig/rbs_rails/active_record.rbs +68 -46
- data/sig/rbs_rails/cli/configuration.rbs +37 -0
- data/sig/rbs_rails/cli.rbs +35 -0
- data/sig/rbs_rails/dependency_builder.rbs +8 -0
- data/sig/rbs_rails/path_helpers.rbs +13 -6
- data/sig/rbs_rails/rake_task.rbs +8 -7
- data/sig/rbs_rails/util/file_writer.rbs +16 -0
- data/sig/rbs_rails/util.rbs +7 -2
- data/sig/rbs_rails/utils/file_writer.rbs +4 -0
- data/sig/rbs_rails/version.rbs +5 -1
- data/sig/rbs_rails.rbs +6 -3
- metadata +33 -14
- data/sig/_internal/activerecord.rbs +0 -4
- data/sig/_internal/fileutils.rbs +0 -4
- data/sig/_internal/thor.rbs +0 -5
- data/sig/parser.rbs +0 -14
- data/sig/rake.rbs +0 -6
|
@@ -1,45 +1,58 @@
|
|
|
1
1
|
module RbsRails
|
|
2
2
|
module ActiveRecord
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
# @rbs klass: untyped
|
|
5
|
+
def self.generatable?(klass) #: boolish
|
|
5
6
|
return false if klass.abstract_class?
|
|
6
7
|
|
|
7
8
|
klass.connection.table_exists?(klass.table_name)
|
|
8
9
|
end
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
# @rbs klass: untyped
|
|
12
|
+
# @rbs dependencies: Array[String]
|
|
13
|
+
def self.class_to_rbs(klass) #: untyped
|
|
14
|
+
Generator.new(klass).generate
|
|
12
15
|
end
|
|
13
16
|
|
|
14
17
|
class Generator
|
|
15
|
-
IGNORED_ENUM_KEYS = %i[_prefix _suffix _default _scopes]
|
|
18
|
+
IGNORED_ENUM_KEYS = %i[_prefix _suffix _default _scopes] #: Array[Symbol]
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
# @rbs @parse_model_file: nil | Parser::AST::Node
|
|
21
|
+
# @rbs @enum_definitions: Array[Hash[Symbol, untyped]]
|
|
22
|
+
# @rbs @klass_name: String
|
|
23
|
+
|
|
24
|
+
attr_reader :dependencies #: DependencyBuilder
|
|
25
|
+
|
|
26
|
+
# @rbs klass: singleton(ActiveRecord::Base) & Enum
|
|
27
|
+
def initialize(klass) #: untyped
|
|
18
28
|
@klass = klass
|
|
19
|
-
@dependencies =
|
|
20
|
-
@klass_name = Util.module_name(klass)
|
|
29
|
+
@dependencies = DependencyBuilder.new
|
|
30
|
+
@klass_name = Util.module_name(klass, abs: false)
|
|
21
31
|
|
|
22
|
-
namespaces = klass_name.split('::').tap{ |names| names.pop }
|
|
32
|
+
namespaces = klass_name(abs: false).split('::').tap{ |names| names.pop }
|
|
23
33
|
@dependencies << namespaces.join('::') unless namespaces.empty?
|
|
24
34
|
end
|
|
25
35
|
|
|
26
|
-
def generate
|
|
36
|
+
def generate #: String
|
|
27
37
|
Util.format_rbs klass_decl
|
|
28
38
|
end
|
|
29
39
|
|
|
30
|
-
private def klass_decl
|
|
40
|
+
private def klass_decl #: String
|
|
31
41
|
<<~RBS
|
|
42
|
+
# resolve-type-names: false
|
|
43
|
+
|
|
32
44
|
#{header}
|
|
33
|
-
extend
|
|
45
|
+
extend ::ActiveRecord::Base::ClassMethods[#{klass_name}, #{relation_class_name}, #{pk_type}]
|
|
34
46
|
|
|
35
47
|
#{columns}
|
|
48
|
+
#{alias_columns}
|
|
36
49
|
#{associations}
|
|
37
50
|
#{generated_association_methods}
|
|
38
51
|
#{has_secure_password}
|
|
39
52
|
#{delegated_type_instance}
|
|
40
53
|
#{delegated_type_scope(singleton: true)}
|
|
41
54
|
#{enum_instance_methods}
|
|
42
|
-
#{
|
|
55
|
+
#{enum_class_methods(singleton: true)}
|
|
43
56
|
#{scopes(singleton: true)}
|
|
44
57
|
|
|
45
58
|
#{generated_relation_methods_decl}
|
|
@@ -49,80 +62,106 @@ module RbsRails
|
|
|
49
62
|
#{collection_proxy_decl}
|
|
50
63
|
|
|
51
64
|
#{footer}
|
|
65
|
+
|
|
66
|
+
#{dependencies.build}
|
|
52
67
|
RBS
|
|
53
68
|
end
|
|
54
69
|
|
|
55
|
-
private def pk_type
|
|
70
|
+
private def pk_type #: String
|
|
56
71
|
pk = klass.primary_key
|
|
57
72
|
return 'top' unless pk
|
|
58
73
|
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
case klass.primary_key
|
|
75
|
+
when Array
|
|
76
|
+
types = klass.columns
|
|
77
|
+
.select { |column| klass.primary_key.include?(column.name) }
|
|
78
|
+
.map { |pk| sql_type_to_class(pk.type) }
|
|
79
|
+
"[#{types.join(' , ')}]"
|
|
80
|
+
else
|
|
81
|
+
col = klass.columns.find { |column| column.name == pk }
|
|
82
|
+
sql_type_to_class(col.type)
|
|
83
|
+
end
|
|
61
84
|
end
|
|
62
85
|
|
|
63
|
-
private def generated_relation_methods_decl
|
|
86
|
+
private def generated_relation_methods_decl #: String
|
|
64
87
|
<<~RBS
|
|
65
|
-
module
|
|
66
|
-
#{
|
|
88
|
+
module #{generated_relation_methods_name}
|
|
89
|
+
#{enum_class_methods(singleton: false)}
|
|
67
90
|
#{scopes(singleton: false)}
|
|
68
91
|
#{delegated_type_scope(singleton: false)}
|
|
69
92
|
end
|
|
70
93
|
RBS
|
|
71
94
|
end
|
|
72
95
|
|
|
73
|
-
private def relation_decl
|
|
96
|
+
private def relation_decl #: String
|
|
74
97
|
<<~RBS
|
|
75
98
|
class #{relation_class_name} < ::ActiveRecord::Relation
|
|
76
|
-
include
|
|
77
|
-
include
|
|
78
|
-
include
|
|
99
|
+
include ::Enumerable[#{klass_name}]
|
|
100
|
+
include #{generated_relation_methods_name}
|
|
101
|
+
include ::ActiveRecord::Relation::Methods[#{klass_name}, #{pk_type}]
|
|
79
102
|
end
|
|
80
103
|
RBS
|
|
81
104
|
end
|
|
82
105
|
|
|
83
|
-
private def collection_proxy_decl
|
|
106
|
+
private def collection_proxy_decl #: String
|
|
84
107
|
<<~RBS
|
|
85
|
-
class ActiveRecord_Associations_CollectionProxy < ::ActiveRecord::Associations::CollectionProxy
|
|
86
|
-
include
|
|
87
|
-
include
|
|
108
|
+
class #{klass_name}::ActiveRecord_Associations_CollectionProxy < ::ActiveRecord::Associations::CollectionProxy
|
|
109
|
+
include ::Enumerable[#{klass_name}]
|
|
110
|
+
include #{generated_relation_methods_name}
|
|
111
|
+
include ::ActiveRecord::Relation::Methods[#{klass_name}, #{pk_type}]
|
|
112
|
+
|
|
113
|
+
def build: (?::ActiveRecord::Associations::CollectionProxy::_EachPair attributes) ?{ () -> untyped } -> #{klass_name}
|
|
114
|
+
| (::Array[::ActiveRecord::Associations::CollectionProxy::_EachPair] attributes) ?{ () -> untyped } -> ::Array[#{klass_name}]
|
|
115
|
+
def create: (?::ActiveRecord::Associations::CollectionProxy::_EachPair attributes) ?{ () -> untyped } -> #{klass_name}
|
|
116
|
+
| (::Array[::ActiveRecord::Associations::CollectionProxy::_EachPair] attributes) ?{ () -> untyped } -> ::Array[#{klass_name}]
|
|
117
|
+
def create!: (?::ActiveRecord::Associations::CollectionProxy::_EachPair attributes) ?{ () -> untyped } -> #{klass_name}
|
|
118
|
+
| (::Array[::ActiveRecord::Associations::CollectionProxy::_EachPair] attributes) ?{ () -> untyped } -> ::Array[#{klass_name}]
|
|
119
|
+
def reload: () -> ::Array[#{klass_name}]
|
|
120
|
+
|
|
121
|
+
def replace: (::Array[#{klass_name}]) -> void
|
|
122
|
+
def delete: (*#{klass_name} | #{pk_type}) -> ::Array[#{klass_name}]
|
|
123
|
+
def destroy: (*#{klass_name} | #{pk_type}) -> ::Array[#{klass_name}]
|
|
124
|
+
def <<: (*#{klass_name} | ::Array[#{klass_name}]) -> self
|
|
125
|
+
def prepend: (*#{klass_name} | ::Array[#{klass_name}]) -> self
|
|
88
126
|
end
|
|
89
127
|
RBS
|
|
90
128
|
end
|
|
91
129
|
|
|
92
|
-
private def header
|
|
130
|
+
private def header #: String
|
|
93
131
|
namespace = +''
|
|
94
|
-
klass_name.split('::').map do |mod_name|
|
|
132
|
+
klass_name(abs: false).split('::').map do |mod_name|
|
|
95
133
|
namespace += "::#{mod_name}"
|
|
96
134
|
mod_object = Object.const_get(namespace)
|
|
97
135
|
case mod_object
|
|
98
136
|
when Class
|
|
99
137
|
# @type var superclass: Class
|
|
100
138
|
superclass = _ = mod_object.superclass
|
|
101
|
-
superclass_name = Util.module_name(superclass)
|
|
139
|
+
superclass_name = Util.module_name(superclass, abs: false)
|
|
102
140
|
@dependencies << superclass_name
|
|
103
141
|
|
|
104
|
-
"class #{
|
|
142
|
+
"class #{namespace} < ::#{superclass_name}"
|
|
105
143
|
when Module
|
|
106
|
-
"module #{
|
|
144
|
+
"module #{namespace}"
|
|
107
145
|
else
|
|
108
146
|
raise 'unreachable'
|
|
109
147
|
end
|
|
110
148
|
end.join("\n")
|
|
111
149
|
end
|
|
112
150
|
|
|
113
|
-
private def footer
|
|
114
|
-
"end\n" * klass_name.split('::').size
|
|
151
|
+
private def footer #: String
|
|
152
|
+
"end\n" * klass_name(abs: false).split('::').size
|
|
115
153
|
end
|
|
116
154
|
|
|
117
|
-
private def associations
|
|
155
|
+
private def associations #: String
|
|
118
156
|
[
|
|
119
157
|
has_many,
|
|
158
|
+
has_and_belongs_to_many,
|
|
120
159
|
has_one,
|
|
121
160
|
belongs_to,
|
|
122
161
|
].join("\n")
|
|
123
162
|
end
|
|
124
163
|
|
|
125
|
-
private def has_many
|
|
164
|
+
private def has_many #: String
|
|
126
165
|
klass.reflect_on_all_associations(:has_many).map do |a|
|
|
127
166
|
@dependencies << a.klass.name
|
|
128
167
|
|
|
@@ -133,14 +172,32 @@ module RbsRails
|
|
|
133
172
|
|
|
134
173
|
<<~RUBY.chomp
|
|
135
174
|
def #{a.name}: () -> #{collection_type}
|
|
136
|
-
def #{a.name}=: (#{collection_type} | Array[#{type}]) -> (#{collection_type} | Array[#{type}])
|
|
137
|
-
def #{singular_name}_ids: () -> Array[Integer]
|
|
138
|
-
def #{singular_name}_ids=: (Array[Integer]) -> Array[Integer]
|
|
175
|
+
def #{a.name}=: (#{collection_type} | ::Array[#{type}]) -> (#{collection_type} | ::Array[#{type}])
|
|
176
|
+
def #{singular_name}_ids: () -> ::Array[::Integer]
|
|
177
|
+
def #{singular_name}_ids=: (::Array[::Integer]) -> ::Array[::Integer]
|
|
178
|
+
RUBY
|
|
179
|
+
end.join("\n")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private def has_and_belongs_to_many #: String
|
|
183
|
+
klass.reflect_on_all_associations(:has_and_belongs_to_many).map do |a|
|
|
184
|
+
@dependencies << a.klass.name
|
|
185
|
+
|
|
186
|
+
singular_name = a.name.to_s.singularize
|
|
187
|
+
type = Util.module_name(a.klass)
|
|
188
|
+
collection_type = "#{type}::ActiveRecord_Associations_CollectionProxy"
|
|
189
|
+
@dependencies << collection_type
|
|
190
|
+
|
|
191
|
+
<<~RUBY.chomp
|
|
192
|
+
def #{a.name}: () -> #{collection_type}
|
|
193
|
+
def #{a.name}=: (#{collection_type} | ::Array[#{type}]) -> (#{collection_type} | ::Array[#{type}])
|
|
194
|
+
def #{singular_name}_ids: () -> ::Array[::Integer]
|
|
195
|
+
def #{singular_name}_ids=: (::Array[::Integer]) -> ::Array[::Integer]
|
|
139
196
|
RUBY
|
|
140
197
|
end.join("\n")
|
|
141
198
|
end
|
|
142
199
|
|
|
143
|
-
private def has_one
|
|
200
|
+
private def has_one #: String
|
|
144
201
|
klass.reflect_on_all_associations(:has_one).map do |a|
|
|
145
202
|
@dependencies << a.klass.name unless a.polymorphic?
|
|
146
203
|
|
|
@@ -149,15 +206,15 @@ module RbsRails
|
|
|
149
206
|
<<~RUBY.chomp
|
|
150
207
|
def #{a.name}: () -> #{type_optional}
|
|
151
208
|
def #{a.name}=: (#{type_optional}) -> #{type_optional}
|
|
152
|
-
def build_#{a.name}: (untyped) -> #{type}
|
|
153
|
-
def create_#{a.name}: (untyped) -> #{type}
|
|
154
|
-
def create_#{a.name}!: (untyped) -> #{type}
|
|
209
|
+
def build_#{a.name}: (?untyped) -> #{type}
|
|
210
|
+
def create_#{a.name}: (?untyped) -> #{type}
|
|
211
|
+
def create_#{a.name}!: (?untyped) -> #{type}
|
|
155
212
|
def reload_#{a.name}: () -> #{type_optional}
|
|
156
213
|
RUBY
|
|
157
214
|
end.join("\n")
|
|
158
215
|
end
|
|
159
216
|
|
|
160
|
-
private def belongs_to
|
|
217
|
+
private def belongs_to #: String
|
|
161
218
|
klass.reflect_on_all_associations(:belongs_to).map do |a|
|
|
162
219
|
@dependencies << a.klass.name unless a.polymorphic?
|
|
163
220
|
|
|
@@ -179,42 +236,43 @@ module RbsRails
|
|
|
179
236
|
end.join("\n")
|
|
180
237
|
end
|
|
181
238
|
|
|
182
|
-
private def generated_association_methods
|
|
239
|
+
private def generated_association_methods #: String
|
|
183
240
|
# @type var sigs: Array[String]
|
|
184
241
|
sigs = []
|
|
185
242
|
|
|
186
243
|
# Needs to require "active_storage/engine"
|
|
187
244
|
if klass.respond_to?(:attachment_reflections)
|
|
188
|
-
sigs << "module GeneratedAssociationMethods"
|
|
245
|
+
sigs << "module #{klass_name}::GeneratedAssociationMethods"
|
|
189
246
|
sigs << klass.attachment_reflections.map do |name, reflection|
|
|
190
247
|
case reflection.macro
|
|
191
248
|
when :has_one_attached
|
|
192
249
|
<<~EOS
|
|
193
|
-
def #{name}: () -> ActiveStorage::Attached::One
|
|
194
|
-
def #{name}=: (ActionDispatch::Http::UploadedFile) -> ActionDispatch::Http::UploadedFile
|
|
195
|
-
| (Rack::Test::UploadedFile) -> Rack::Test::UploadedFile
|
|
196
|
-
| (ActiveStorage::Blob) -> ActiveStorage::Blob
|
|
197
|
-
| (String) -> String
|
|
198
|
-
| ({ io: IO, filename: String, content_type: String? }) -> { io: IO, filename: String, content_type: String? }
|
|
250
|
+
def #{name}: () -> ::ActiveStorage::Attached::One
|
|
251
|
+
def #{name}=: (::ActionDispatch::Http::UploadedFile) -> ::ActionDispatch::Http::UploadedFile
|
|
252
|
+
| (::Rack::Test::UploadedFile) -> ::Rack::Test::UploadedFile
|
|
253
|
+
| (::ActiveStorage::Blob) -> ::ActiveStorage::Blob
|
|
254
|
+
| (::String) -> ::String
|
|
255
|
+
| ({ io: ::IO, filename: ::String, content_type: ::String? }) -> { io: ::IO, filename: ::String, content_type: ::String? }
|
|
199
256
|
| (nil) -> nil
|
|
200
257
|
EOS
|
|
201
258
|
when :has_many_attached
|
|
202
259
|
<<~EOS
|
|
203
|
-
def #{name}: () -> ActiveStorage::Attached::Many
|
|
260
|
+
def #{name}: () -> ::ActiveStorage::Attached::Many
|
|
204
261
|
def #{name}=: (untyped) -> untyped
|
|
205
262
|
EOS
|
|
206
263
|
else
|
|
207
|
-
raise
|
|
264
|
+
raise "unknown macro: #{reflection.macro}"
|
|
208
265
|
end
|
|
209
266
|
end.join("\n")
|
|
210
267
|
sigs << "end"
|
|
211
|
-
sigs << "include GeneratedAssociationMethods"
|
|
268
|
+
sigs << "include #{klass_name}::GeneratedAssociationMethods"
|
|
212
269
|
end
|
|
213
270
|
|
|
214
271
|
sigs.join("\n")
|
|
215
272
|
end
|
|
216
273
|
|
|
217
|
-
|
|
274
|
+
# @rbs singleton: bool
|
|
275
|
+
private def delegated_type_scope(singleton:) #: String
|
|
218
276
|
definitions = delegated_type_definitions
|
|
219
277
|
return "" unless definitions
|
|
220
278
|
definitions.map do |definition|
|
|
@@ -225,14 +283,14 @@ module RbsRails
|
|
|
225
283
|
end.flatten.join("\n")
|
|
226
284
|
end
|
|
227
285
|
|
|
228
|
-
private def delegated_type_instance
|
|
286
|
+
private def delegated_type_instance #: String
|
|
229
287
|
definitions = delegated_type_definitions
|
|
230
288
|
return "" unless definitions
|
|
231
289
|
# @type var methods: Array[String]
|
|
232
290
|
methods = []
|
|
233
291
|
definitions.each do |definition|
|
|
234
|
-
methods << "def #{definition[:role]}_class: () -> Class"
|
|
235
|
-
methods << "def #{definition[:role]}_name: () -> String"
|
|
292
|
+
methods << "def #{definition[:role]}_class: () -> ::Class"
|
|
293
|
+
methods << "def #{definition[:role]}_name: () -> ::String"
|
|
236
294
|
methods << definition[:types].map do |type|
|
|
237
295
|
scope_name = type.tableize.gsub("/", "_")
|
|
238
296
|
singular = scope_name.singularize
|
|
@@ -246,7 +304,7 @@ module RbsRails
|
|
|
246
304
|
methods.join("\n")
|
|
247
305
|
end
|
|
248
306
|
|
|
249
|
-
private def delegated_type_definitions
|
|
307
|
+
private def delegated_type_definitions #: Array[{ role: Symbol, types: Array[String] }]?
|
|
250
308
|
ast = parse_model_file
|
|
251
309
|
return unless ast
|
|
252
310
|
|
|
@@ -285,7 +343,7 @@ module RbsRails
|
|
|
285
343
|
end.compact
|
|
286
344
|
end
|
|
287
345
|
|
|
288
|
-
private def has_secure_password
|
|
346
|
+
private def has_secure_password #: String?
|
|
289
347
|
ast = parse_model_file
|
|
290
348
|
return unless ast
|
|
291
349
|
|
|
@@ -303,99 +361,47 @@ module RbsRails
|
|
|
303
361
|
end
|
|
304
362
|
|
|
305
363
|
<<~EOS
|
|
306
|
-
module ActiveModel_SecurePassword_InstanceMethodsOnActivation_#{attribute}
|
|
307
|
-
attr_reader #{attribute}: String?
|
|
308
|
-
def #{attribute}=: (String) -> String
|
|
309
|
-
def #{attribute}_confirmation=: (String) -> String
|
|
310
|
-
def authenticate_#{attribute}: (String) -> (#{klass_name} | false)
|
|
364
|
+
module #{klass_name}::ActiveModel_SecurePassword_InstanceMethodsOnActivation_#{attribute}
|
|
365
|
+
attr_reader #{attribute}: ::String?
|
|
366
|
+
def #{attribute}=: (::String) -> ::String
|
|
367
|
+
def #{attribute}_confirmation=: (::String) -> ::String
|
|
368
|
+
def authenticate_#{attribute}: (::String) -> (#{klass_name} | false)
|
|
311
369
|
#{attribute == :password ? "alias authenticate authenticate_password" : ""}
|
|
312
370
|
end
|
|
313
|
-
include ActiveModel_SecurePassword_InstanceMethodsOnActivation_#{attribute}
|
|
371
|
+
include #{klass_name}::ActiveModel_SecurePassword_InstanceMethodsOnActivation_#{attribute}
|
|
314
372
|
EOS
|
|
315
373
|
end.compact.join("\n")
|
|
316
374
|
end
|
|
317
375
|
|
|
318
|
-
private def enum_instance_methods
|
|
376
|
+
private def enum_instance_methods #: String
|
|
319
377
|
# @type var methods: Array[String]
|
|
320
378
|
methods = []
|
|
321
|
-
enum_definitions.each do |
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
values.each do |label, value|
|
|
326
|
-
value_method_name = enum_method_name(hash, name, label)
|
|
327
|
-
methods << "def #{value_method_name}!: () -> bool"
|
|
328
|
-
methods << "def #{value_method_name}?: () -> bool"
|
|
329
|
-
end
|
|
330
|
-
end
|
|
379
|
+
klass.enum_definitions.each do |_, method_name|
|
|
380
|
+
methods << "def #{method_name}!: () -> bool"
|
|
381
|
+
methods << "def #{method_name}?: () -> bool"
|
|
331
382
|
end
|
|
332
383
|
|
|
333
384
|
methods.join("\n")
|
|
334
385
|
end
|
|
335
386
|
|
|
336
|
-
|
|
387
|
+
# @rbs singleton: untyped
|
|
388
|
+
private def enum_class_methods(singleton:) #: String
|
|
337
389
|
# @type var methods: Array[String]
|
|
338
390
|
methods = []
|
|
339
|
-
enum_definitions.each do |
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
values.each do |label, value|
|
|
344
|
-
value_method_name = enum_method_name(hash, name, label)
|
|
345
|
-
methods << "def #{singleton ? 'self.' : ''}#{value_method_name}: () -> #{relation_class_name}"
|
|
346
|
-
end
|
|
347
|
-
end
|
|
348
|
-
end
|
|
349
|
-
methods.join("\n")
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
private def enum_definitions
|
|
353
|
-
@enum_definitions ||= build_enum_definitions
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
# We need static analysis to detect enum.
|
|
357
|
-
# ActiveRecord has `defined_enums` method,
|
|
358
|
-
# but it does not contain _prefix and _suffix information.
|
|
359
|
-
private def build_enum_definitions
|
|
360
|
-
ast = parse_model_file
|
|
361
|
-
return [] unless ast
|
|
362
|
-
|
|
363
|
-
traverse(ast).map do |node|
|
|
364
|
-
# @type block: nil | Hash[untyped, untyped]
|
|
365
|
-
next unless node.type == :send
|
|
366
|
-
next unless node.children[0].nil?
|
|
367
|
-
next unless node.children[1] == :enum
|
|
368
|
-
|
|
369
|
-
definitions = node.children[2]
|
|
370
|
-
next unless definitions
|
|
371
|
-
next unless definitions.type == :hash
|
|
372
|
-
next unless traverse(definitions).all? { |n| [:str, :sym, :int, :hash, :pair, :true, :false].include?(n.type) }
|
|
373
|
-
|
|
374
|
-
code = definitions.loc.expression.source
|
|
375
|
-
code = "{#{code}}" if code[0] != '{'
|
|
376
|
-
eval(code)
|
|
377
|
-
end.compact
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
private def enum_method_name(hash, name, label)
|
|
381
|
-
enum_prefix = hash[:_prefix]
|
|
382
|
-
enum_suffix = hash[:_suffix]
|
|
383
|
-
|
|
384
|
-
if enum_prefix == true
|
|
385
|
-
prefix = "#{name}_"
|
|
386
|
-
elsif enum_prefix
|
|
387
|
-
prefix = "#{enum_prefix}_"
|
|
391
|
+
klass.enum_definitions.map(&:first).uniq.each do |name|
|
|
392
|
+
column = klass.columns_hash[name.to_s] || klass.columns_hash[klass.attribute_aliases[name.to_s]]
|
|
393
|
+
class_name = sql_type_to_class(column.type)
|
|
394
|
+
methods << "def #{singleton ? 'self.' : ''}#{name.to_s.pluralize}: () -> ::ActiveSupport::HashWithIndifferentAccess[::String, #{class_name}]"
|
|
388
395
|
end
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
suffix = "_#{enum_suffix}"
|
|
396
|
+
klass.enum_definitions.each do |_, method_name|
|
|
397
|
+
methods << "def #{singleton ? 'self.' : ''}#{method_name}: () -> #{relation_class_name}"
|
|
398
|
+
methods << "def #{singleton ? 'self.' : ''}not_#{method_name}: () -> #{relation_class_name}"
|
|
393
399
|
end
|
|
394
|
-
|
|
395
|
-
"#{prefix}#{label}#{suffix}"
|
|
400
|
+
methods.join("\n")
|
|
396
401
|
end
|
|
397
402
|
|
|
398
|
-
|
|
403
|
+
# @rbs singleton: untyped
|
|
404
|
+
private def scopes(singleton:) #: untyped
|
|
399
405
|
ast = parse_model_file
|
|
400
406
|
return '' unless ast
|
|
401
407
|
|
|
@@ -429,7 +435,8 @@ module RbsRails
|
|
|
429
435
|
sigs.join("\n")
|
|
430
436
|
end
|
|
431
437
|
|
|
432
|
-
|
|
438
|
+
# @rbs args_node: untyped
|
|
439
|
+
private def args_to_type(args_node) #: untyped
|
|
433
440
|
# @type var res: Array[String]
|
|
434
441
|
res = []
|
|
435
442
|
# @type var block: String?
|
|
@@ -457,97 +464,188 @@ module RbsRails
|
|
|
457
464
|
"(#{res.join(", ")})#{block}"
|
|
458
465
|
end
|
|
459
466
|
|
|
460
|
-
private def parse_model_file
|
|
467
|
+
private def parse_model_file #: untyped
|
|
461
468
|
return @parse_model_file if defined?(@parse_model_file)
|
|
462
469
|
|
|
463
|
-
path =
|
|
464
|
-
return @parse_model_file = nil unless path
|
|
465
|
-
return [] unless path.exist?
|
|
470
|
+
path, _line = Object.const_source_location(klass.name) rescue nil
|
|
471
|
+
return @parse_model_file = nil unless path
|
|
466
472
|
|
|
467
|
-
|
|
468
|
-
|
|
473
|
+
begin
|
|
474
|
+
@parse_model_file = parser_class.parse File.read(path)
|
|
475
|
+
rescue => e
|
|
476
|
+
@parse_model_file = nil
|
|
477
|
+
end
|
|
478
|
+
end
|
|
469
479
|
|
|
470
|
-
|
|
480
|
+
private def parser_class #: untyped
|
|
481
|
+
case RUBY_VERSION
|
|
482
|
+
when /\A3\.2\./
|
|
483
|
+
# backward campatibility
|
|
484
|
+
require 'parser/ruby32'
|
|
485
|
+
Parser::Ruby32
|
|
486
|
+
when /\A3\.3\./
|
|
487
|
+
Prism::Translation::Parser33 # steep:ignore
|
|
488
|
+
when /\A3\.4\./
|
|
489
|
+
Prism::Translation::Parser34 # steep:ignore
|
|
490
|
+
else
|
|
491
|
+
# For Prism v1.5.0+, Prism::Translation::ParserCurrent should be used instead.
|
|
492
|
+
Prism::Translation::Parser34 # steep:ignore
|
|
493
|
+
end
|
|
471
494
|
end
|
|
472
495
|
|
|
496
|
+
#: (Parser::AST::Node) { (Parser::AST::Node) -> untyped } -> untyped
|
|
497
|
+
#: (Parser::AST::Node) -> Enumerator[Parser::AST::Node, untyped]
|
|
473
498
|
private def traverse(node, &block)
|
|
474
|
-
return to_enum(__method__ || raise, node) unless
|
|
499
|
+
return to_enum(__method__ || raise, node) unless block
|
|
475
500
|
|
|
476
|
-
# @type var block: ^(Parser::AST::Node) -> untyped
|
|
477
501
|
block.call node
|
|
478
502
|
node.children.each do |child|
|
|
479
503
|
traverse(child, &block) if child.is_a?(Parser::AST::Node)
|
|
480
504
|
end
|
|
481
505
|
end
|
|
482
506
|
|
|
483
|
-
private def relation_class_name
|
|
484
|
-
"ActiveRecord_Relation"
|
|
507
|
+
private def relation_class_name #: String
|
|
508
|
+
"#{klass_name}::ActiveRecord_Relation"
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# @rbs abs: boolish
|
|
512
|
+
private def klass_name(abs: true) #: String
|
|
513
|
+
abs ? "::#{@klass_name}" : @klass_name
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
private def generated_relation_methods_name #: String
|
|
517
|
+
"#{klass_name}::GeneratedRelationMethods"
|
|
485
518
|
end
|
|
486
519
|
|
|
487
|
-
|
|
488
|
-
|
|
520
|
+
|
|
521
|
+
private def columns #: untyped
|
|
522
|
+
mod_sig = +"module #{klass_name}::GeneratedAttributeMethods\n"
|
|
489
523
|
mod_sig << klass.columns.map do |col|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
524
|
+
# NOTE:
|
|
525
|
+
# `klass.attribute_types[col.name].try(:coder)` is for Rails 6.0 and before
|
|
526
|
+
# `klass.attribute_types[col.name]&.instance_variable_get(:@coder)` is for Rails 6.1 and after
|
|
527
|
+
col_serializer = klass.attribute_types[col.name].try(:coder) ||
|
|
528
|
+
klass.attribute_types[col.name]&.instance_variable_get(:@coder)
|
|
529
|
+
# e.g. ActiveRecord::Coders::JSON
|
|
530
|
+
# if your model has `serialize ..., JSON`
|
|
531
|
+
# e.g. #<ActiveRecord::Coders::YAMLColumn:0x0000aaaafdc54970 @attr_name=..., @object_class=Array>
|
|
532
|
+
# if your model has `serialize ..., Array`
|
|
533
|
+
# etc.
|
|
534
|
+
col_serialize_to = col_serializer.try(:object_class)&.name
|
|
535
|
+
if col_serializer.is_a?(Class) && col_serializer.name == 'ActiveRecord::Coders::JSON'
|
|
536
|
+
class_name = 'untyped' # JSON
|
|
537
|
+
elsif col_serialize_to == 'Array'
|
|
538
|
+
class_name = '::Array[untyped]' # Array
|
|
539
|
+
elsif col_serialize_to == 'Hash'
|
|
540
|
+
class_name = '::Hash[untyped, untyped]' # Hash
|
|
541
|
+
else
|
|
542
|
+
class_name = if klass.enum_definitions.any? { |name, _| name == col.name.to_sym }
|
|
543
|
+
'::String'
|
|
544
|
+
else
|
|
545
|
+
sql_type_to_class(col.type)
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
sql_class_name = col.type == :datetime ? '::Time' : sql_type_to_class(col.type)
|
|
549
|
+
# If the DB says the column can be null, we need `<type>?`
|
|
550
|
+
# ...but if the type is already `untyped` there's no point in writing `untyped?`
|
|
551
|
+
class_name_opt = (class_name == 'untyped') ? 'untyped' : optional(class_name)
|
|
496
552
|
column_type = col.null ? class_name_opt : class_name
|
|
553
|
+
sql_column_type = col.null ? optional(sql_class_name) : sql_class_name
|
|
497
554
|
sig = <<~EOS
|
|
498
555
|
def #{col.name}: () -> #{column_type}
|
|
499
556
|
def #{col.name}=: (#{column_type}) -> #{column_type}
|
|
500
557
|
def #{col.name}?: () -> bool
|
|
501
|
-
def #{col.name}_changed?: () -> bool
|
|
558
|
+
def #{col.name}_changed?: (?from: #{class_name_opt}, ?to: #{class_name_opt}) -> bool
|
|
502
559
|
def #{col.name}_change: () -> [#{class_name_opt}, #{class_name_opt}]
|
|
503
560
|
def #{col.name}_will_change!: () -> void
|
|
504
561
|
def #{col.name}_was: () -> #{class_name_opt}
|
|
505
|
-
def #{col.name}_previously_changed?: () -> bool
|
|
506
|
-
def #{col.name}_previous_change: () -> Array[#{class_name_opt}]?
|
|
562
|
+
def #{col.name}_previously_changed?: (?from: #{class_name_opt}, ?to: #{class_name_opt}) -> bool
|
|
563
|
+
def #{col.name}_previous_change: () -> ::Array[#{class_name_opt}]?
|
|
507
564
|
def #{col.name}_previously_was: () -> #{class_name_opt}
|
|
508
565
|
def #{col.name}_before_last_save: () -> #{class_name_opt}
|
|
509
|
-
def #{col.name}_change_to_be_saved: () -> Array[#{class_name_opt}]?
|
|
566
|
+
def #{col.name}_change_to_be_saved: () -> ::Array[#{class_name_opt}]?
|
|
510
567
|
def #{col.name}_in_database: () -> #{class_name_opt}
|
|
511
|
-
def saved_change_to_#{col.name}: () -> Array[#{class_name_opt}]?
|
|
568
|
+
def saved_change_to_#{col.name}: () -> ::Array[#{class_name_opt}]?
|
|
512
569
|
def saved_change_to_#{col.name}?: () -> bool
|
|
513
570
|
def will_save_change_to_#{col.name}?: () -> bool
|
|
514
571
|
def restore_#{col.name}!: () -> void
|
|
515
572
|
def clear_#{col.name}_change: () -> void
|
|
573
|
+
def #{col.name}_before_type_cast: () -> #{sql_column_type}
|
|
574
|
+
def #{col.name}_for_database: () -> #{sql_column_type}
|
|
575
|
+
EOS
|
|
576
|
+
sig << "\n"
|
|
577
|
+
sig
|
|
578
|
+
end.join("\n")
|
|
579
|
+
mod_sig << "\nend\n"
|
|
580
|
+
mod_sig << "include #{klass_name}::GeneratedAttributeMethods"
|
|
581
|
+
mod_sig
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
private def alias_columns
|
|
585
|
+
attribute_aliases = klass.attribute_aliases.dup
|
|
586
|
+
attribute_aliases["id_value"] ||= "id" if klass.attribute_names.include?("id")
|
|
587
|
+
|
|
588
|
+
mod_sig = +"module #{klass_name}::GeneratedAliasAttributeMethods\n"
|
|
589
|
+
mod_sig << "include #{klass_name}::GeneratedAttributeMethods\n"
|
|
590
|
+
mod_sig << attribute_aliases.map do |col|
|
|
591
|
+
sig = <<~EOS
|
|
592
|
+
alias #{col[0]} #{col[1]}
|
|
593
|
+
alias #{col[0]}= #{col[1]}=
|
|
594
|
+
alias #{col[0]}? #{col[1]}?
|
|
595
|
+
alias #{col[0]}_changed? #{col[1]}_changed?
|
|
596
|
+
alias #{col[0]}_change #{col[1]}_change
|
|
597
|
+
alias #{col[0]}_will_change! #{col[1]}_will_change!
|
|
598
|
+
alias #{col[0]}_was #{col[1]}_was
|
|
599
|
+
alias #{col[0]}_previously_changed? #{col[1]}_previously_changed?
|
|
600
|
+
alias #{col[0]}_previous_change #{col[1]}_previous_change
|
|
601
|
+
alias #{col[0]}_previously_was #{col[1]}_previously_was
|
|
602
|
+
alias #{col[0]}_before_last_save #{col[1]}_before_last_save
|
|
603
|
+
alias #{col[0]}_change_to_be_saved #{col[1]}_change_to_be_saved
|
|
604
|
+
alias #{col[0]}_in_database #{col[1]}_in_database
|
|
605
|
+
alias saved_change_to_#{col[0]} saved_change_to_#{col[1]}
|
|
606
|
+
alias saved_change_to_#{col[0]}? saved_change_to_#{col[1]}?
|
|
607
|
+
alias will_save_change_to_#{col[0]}? will_save_change_to_#{col[1]}?
|
|
608
|
+
alias restore_#{col[0]}! restore_#{col[1]}!
|
|
609
|
+
alias clear_#{col[0]}_change clear_#{col[1]}_change
|
|
610
|
+
alias #{col[0]}_before_type_cast #{col[1]}_before_type_cast
|
|
611
|
+
alias #{col[0]}_for_database #{col[1]}_for_database
|
|
516
612
|
EOS
|
|
517
613
|
sig << "\n"
|
|
518
614
|
sig
|
|
519
615
|
end.join("\n")
|
|
520
616
|
mod_sig << "\nend\n"
|
|
521
|
-
mod_sig << "include
|
|
617
|
+
mod_sig << "include #{klass_name}::GeneratedAliasAttributeMethods"
|
|
522
618
|
mod_sig
|
|
523
619
|
end
|
|
524
620
|
|
|
525
|
-
|
|
621
|
+
# @rbs class_name: String
|
|
622
|
+
private def optional(class_name) #: String
|
|
526
623
|
class_name.include?("|") ? "(#{class_name})?" : "#{class_name}?"
|
|
527
624
|
end
|
|
528
625
|
|
|
529
|
-
|
|
626
|
+
# @rbs t: untyped
|
|
627
|
+
private def sql_type_to_class(t) #: untyped
|
|
530
628
|
case t
|
|
531
629
|
when :integer
|
|
532
|
-
'Integer'
|
|
630
|
+
'::Integer'
|
|
533
631
|
when :float
|
|
534
|
-
'Float'
|
|
632
|
+
'::Float'
|
|
535
633
|
when :decimal
|
|
536
|
-
'BigDecimal'
|
|
634
|
+
'::BigDecimal'
|
|
537
635
|
when :string, :text, :citext, :uuid, :binary
|
|
538
|
-
'String'
|
|
636
|
+
'::String'
|
|
539
637
|
when :datetime
|
|
540
|
-
'ActiveSupport::TimeWithZone'
|
|
638
|
+
'::ActiveSupport::TimeWithZone'
|
|
541
639
|
when :boolean
|
|
542
640
|
"bool"
|
|
543
641
|
when :jsonb, :json
|
|
544
642
|
"untyped"
|
|
545
643
|
when :date
|
|
546
|
-
'Date'
|
|
644
|
+
'::Date'
|
|
547
645
|
when :time
|
|
548
|
-
'Time'
|
|
646
|
+
'::Time'
|
|
549
647
|
when :inet
|
|
550
|
-
"IPAddr"
|
|
648
|
+
"::IPAddr"
|
|
551
649
|
else
|
|
552
650
|
# Unknown column type, give up
|
|
553
651
|
'untyped'
|
|
@@ -555,7 +653,7 @@ module RbsRails
|
|
|
555
653
|
end
|
|
556
654
|
|
|
557
655
|
private
|
|
558
|
-
attr_reader :klass
|
|
656
|
+
attr_reader :klass #: singleton(ActiveRecord::Base) & Enum
|
|
559
657
|
end
|
|
560
658
|
end
|
|
561
659
|
end
|