rbs_rails 0.12.1 → 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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/.github/workflows/release.yml +27 -0
  4. data/CHANGELOG.md +56 -0
  5. data/Gemfile +7 -1
  6. data/Gemfile.lock +111 -93
  7. data/README.md +46 -1
  8. data/Rakefile +7 -2
  9. data/example/rbs_rails.rb +27 -0
  10. data/exe/rbs_rails +6 -0
  11. data/lib/generators/rbs_rails/install_generator.rb +1 -10
  12. data/lib/rbs_rails/active_record/enum.rb +81 -0
  13. data/lib/rbs_rails/active_record.rb +233 -143
  14. data/lib/rbs_rails/cli/configuration.rb +66 -0
  15. data/lib/rbs_rails/cli.rb +173 -0
  16. data/lib/rbs_rails/dependency_builder.rb +25 -8
  17. data/lib/rbs_rails/path_helpers.rb +14 -2
  18. data/lib/rbs_rails/rake_task.rb +38 -40
  19. data/lib/rbs_rails/util/file_writer.rb +22 -0
  20. data/lib/rbs_rails/util.rb +11 -4
  21. data/lib/rbs_rails/version.rb +1 -1
  22. data/lib/rbs_rails.rb +5 -2
  23. data/rbs_collection.lock.yaml +86 -38
  24. data/rbs_collection.yaml +1 -16
  25. data/rbs_rails.gemspec +1 -0
  26. data/sig/{install_generator.rbs → generators/rbs_rails/install_generator.rbs} +2 -0
  27. data/sig/rbs_rails/active_record/enum.rbs +26 -0
  28. data/sig/rbs_rails/active_record.rbs +67 -49
  29. data/sig/rbs_rails/cli/configuration.rbs +37 -0
  30. data/sig/rbs_rails/cli.rbs +35 -0
  31. data/sig/rbs_rails/dependency_builder.rbs +7 -3
  32. data/sig/rbs_rails/path_helpers.rbs +13 -6
  33. data/sig/rbs_rails/rake_task.rbs +7 -6
  34. data/sig/rbs_rails/util/file_writer.rbs +16 -0
  35. data/sig/rbs_rails/util.rbs +7 -2
  36. data/sig/rbs_rails/utils/file_writer.rbs +4 -0
  37. data/sig/rbs_rails/version.rbs +5 -1
  38. data/sig/rbs_rails.rbs +6 -3
  39. metadata +32 -8
  40. data/sig/_internal/activerecord.rbs +0 -4
  41. data/sig/parser.rbs +0 -14
  42. data/sig/rake.rbs +0 -6
@@ -1,45 +1,58 @@
1
1
  module RbsRails
2
2
  module ActiveRecord
3
3
 
4
- def self.generatable?(klass)
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
- def self.class_to_rbs(klass, dependencies: [])
11
- Generator.new(klass, dependencies: dependencies).generate
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
- def initialize(klass, dependencies:)
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 = dependencies
29
+ @dependencies = DependencyBuilder.new
20
30
  @klass_name = Util.module_name(klass, abs: false)
21
31
 
22
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 ::_ActiveRecord_Relation_ClassMethods[#{klass_name}, #{relation_class_name}, #{pk_type}]
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
- #{enum_scope_methods(singleton: true)}
55
+ #{enum_class_methods(singleton: true)}
43
56
  #{scopes(singleton: true)}
44
57
 
45
58
  #{generated_relation_methods_decl}
@@ -49,47 +62,72 @@ 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
- col = klass.columns.find {|col| col.name == pk }
60
- sql_type_to_class(col.type)
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 #{generated_relation_methods_name(abs: false)}
66
- #{enum_scope_methods(singleton: false)}
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
- class #{relation_class_name(abs: false)} < ::ActiveRecord::Relation
76
- include #{generated_relation_methods_name}
77
- include ::_ActiveRecord_Relation[#{klass_name}, #{pk_type}]
98
+ class #{relation_class_name} < ::ActiveRecord::Relation
78
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
108
+ class #{klass_name}::ActiveRecord_Associations_CollectionProxy < ::ActiveRecord::Associations::CollectionProxy
109
+ include ::Enumerable[#{klass_name}]
86
110
  include #{generated_relation_methods_name}
87
- include ::_ActiveRecord_Relation[#{klass_name}, #{pk_type}]
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
132
  klass_name(abs: false).split('::').map do |mod_name|
95
133
  namespace += "::#{mod_name}"
@@ -101,28 +139,29 @@ module RbsRails
101
139
  superclass_name = Util.module_name(superclass, abs: false)
102
140
  @dependencies << superclass_name
103
141
 
104
- "class #{mod_name} < ::#{superclass_name}"
142
+ "class #{namespace} < ::#{superclass_name}"
105
143
  when Module
106
- "module #{mod_name}"
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
151
+ private def footer #: String
114
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
 
@@ -140,7 +179,25 @@ module RbsRails
140
179
  end.join("\n")
141
180
  end
142
181
 
143
- private def has_one
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]
196
+ RUBY
197
+ end.join("\n")
198
+ end
199
+
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
 
@@ -150,14 +207,14 @@ module RbsRails
150
207
  def #{a.name}: () -> #{type_optional}
151
208
  def #{a.name}=: (#{type_optional}) -> #{type_optional}
152
209
  def build_#{a.name}: (?untyped) -> #{type}
153
- def create_#{a.name}: (untyped) -> #{type}
154
- def create_#{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,13 +236,13 @@ 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
@@ -208,13 +265,14 @@ module RbsRails
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
- private def delegated_type_scope(singleton:)
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 |hash|
322
- hash.each do |name, values|
323
- next if IGNORED_ENUM_KEYS.include?(name)
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
- private def enum_scope_methods(singleton:)
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 |hash|
340
- hash.each do |name, values|
341
- next if IGNORED_ENUM_KEYS.include?(name)
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
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}]"
348
395
  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}_"
388
- end
389
- if enum_suffix == true
390
- suffix = "_#{name}"
391
- elsif enum_suffix
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
- private def scopes(singleton:)
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
- private def args_to_type(args_node)
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,19 +464,37 @@ 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 = Rails.root.join('app/models/', klass_name(abs: false).underscore + '.rb')
464
- return @parse_model_file = nil unless path.exist?
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
- ast = Parser::CurrentRuby.parse path.read
468
- return @parse_model_file = nil unless path.exist?
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
- @parse_model_file = ast
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
499
  return to_enum(__method__ || raise, node) unless block
475
500
 
@@ -479,38 +504,62 @@ module RbsRails
479
504
  end
480
505
  end
481
506
 
482
- private def relation_class_name(abs: true)
483
- abs ? "#{klass_name}::ActiveRecord_Relation" : "ActiveRecord_Relation"
507
+ private def relation_class_name #: String
508
+ "#{klass_name}::ActiveRecord_Relation"
484
509
  end
485
510
 
486
- private def klass_name(abs: true)
511
+ # @rbs abs: boolish
512
+ private def klass_name(abs: true) #: String
487
513
  abs ? "::#{@klass_name}" : @klass_name
488
514
  end
489
515
 
490
- private def generated_relation_methods_name(abs: true)
491
- abs ? "#{klass_name}::GeneratedRelationMethods" : "GeneratedRelationMethods"
516
+ private def generated_relation_methods_name #: String
517
+ "#{klass_name}::GeneratedRelationMethods"
492
518
  end
493
519
 
494
520
 
495
- private def columns
496
- mod_sig = +"module GeneratedAttributeMethods\n"
521
+ private def columns #: untyped
522
+ mod_sig = +"module #{klass_name}::GeneratedAttributeMethods\n"
497
523
  mod_sig << klass.columns.map do |col|
498
- class_name = if enum_definitions.any? { |hash| hash.key?(col.name) || hash.key?(col.name.to_sym) }
499
- '::String'
500
- else
501
- sql_type_to_class(col.type)
502
- end
503
- class_name_opt = optional(class_name)
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)
504
552
  column_type = col.null ? class_name_opt : class_name
553
+ sql_column_type = col.null ? optional(sql_class_name) : sql_class_name
505
554
  sig = <<~EOS
506
555
  def #{col.name}: () -> #{column_type}
507
556
  def #{col.name}=: (#{column_type}) -> #{column_type}
508
557
  def #{col.name}?: () -> bool
509
- def #{col.name}_changed?: () -> bool
558
+ def #{col.name}_changed?: (?from: #{class_name_opt}, ?to: #{class_name_opt}) -> bool
510
559
  def #{col.name}_change: () -> [#{class_name_opt}, #{class_name_opt}]
511
560
  def #{col.name}_will_change!: () -> void
512
561
  def #{col.name}_was: () -> #{class_name_opt}
513
- def #{col.name}_previously_changed?: () -> bool
562
+ def #{col.name}_previously_changed?: (?from: #{class_name_opt}, ?to: #{class_name_opt}) -> bool
514
563
  def #{col.name}_previous_change: () -> ::Array[#{class_name_opt}]?
515
564
  def #{col.name}_previously_was: () -> #{class_name_opt}
516
565
  def #{col.name}_before_last_save: () -> #{class_name_opt}
@@ -521,20 +570,61 @@ module RbsRails
521
570
  def will_save_change_to_#{col.name}?: () -> bool
522
571
  def restore_#{col.name}!: () -> void
523
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
524
612
  EOS
525
613
  sig << "\n"
526
614
  sig
527
615
  end.join("\n")
528
616
  mod_sig << "\nend\n"
529
- mod_sig << "include GeneratedAttributeMethods"
617
+ mod_sig << "include #{klass_name}::GeneratedAliasAttributeMethods"
530
618
  mod_sig
531
619
  end
532
620
 
533
- private def optional(class_name)
621
+ # @rbs class_name: String
622
+ private def optional(class_name) #: String
534
623
  class_name.include?("|") ? "(#{class_name})?" : "#{class_name}?"
535
624
  end
536
625
 
537
- private def sql_type_to_class(t)
626
+ # @rbs t: untyped
627
+ private def sql_type_to_class(t) #: untyped
538
628
  case t
539
629
  when :integer
540
630
  '::Integer'
@@ -563,7 +653,7 @@ module RbsRails
563
653
  end
564
654
 
565
655
  private
566
- attr_reader :klass
656
+ attr_reader :klass #: singleton(ActiveRecord::Base) & Enum
567
657
  end
568
658
  end
569
659
  end