tapioca 0.2.7 → 0.4.1
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/Gemfile +27 -1
- data/README.md +21 -2
- data/Rakefile +15 -4
- data/lib/tapioca.rb +15 -9
- data/lib/tapioca/cli.rb +41 -12
- data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +129 -0
- data/lib/tapioca/compilers/dsl/action_mailer.rb +65 -0
- data/lib/tapioca/compilers/dsl/active_record_associations.rb +285 -0
- data/lib/tapioca/compilers/dsl/active_record_columns.rb +379 -0
- data/lib/tapioca/compilers/dsl/active_record_enum.rb +112 -0
- data/lib/tapioca/compilers/dsl/active_record_identity_cache.rb +213 -0
- data/lib/tapioca/compilers/dsl/active_record_scope.rb +100 -0
- data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +170 -0
- data/lib/tapioca/compilers/dsl/active_resource.rb +140 -0
- data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +126 -0
- data/lib/tapioca/compilers/dsl/base.rb +163 -0
- data/lib/tapioca/compilers/dsl/frozen_record.rb +96 -0
- data/lib/tapioca/compilers/dsl/protobuf.rb +144 -0
- data/lib/tapioca/compilers/dsl/smart_properties.rb +173 -0
- data/lib/tapioca/compilers/dsl/state_machines.rb +378 -0
- data/lib/tapioca/compilers/dsl/url_helpers.rb +83 -0
- data/lib/tapioca/compilers/dsl_compiler.rb +121 -0
- data/lib/tapioca/compilers/requires_compiler.rb +67 -0
- data/lib/tapioca/compilers/sorbet.rb +34 -0
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +209 -49
- data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +3 -17
- data/lib/tapioca/compilers/todos_compiler.rb +32 -0
- data/lib/tapioca/config.rb +42 -0
- data/lib/tapioca/config_builder.rb +75 -0
- data/lib/tapioca/constant_locator.rb +1 -0
- data/lib/tapioca/core_ext/class.rb +23 -0
- data/lib/tapioca/gemfile.rb +14 -1
- data/lib/tapioca/generator.rb +235 -67
- data/lib/tapioca/loader.rb +20 -9
- data/lib/tapioca/sorbet_config_parser.rb +77 -0
- data/lib/tapioca/version.rb +1 -1
- metadata +35 -66
@@ -0,0 +1,285 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "parlour"
|
5
|
+
|
6
|
+
begin
|
7
|
+
require "active_record"
|
8
|
+
rescue LoadError
|
9
|
+
return
|
10
|
+
end
|
11
|
+
|
12
|
+
module Tapioca
|
13
|
+
module Compilers
|
14
|
+
module Dsl
|
15
|
+
# `Tapioca::Compilers::Dsl::ActiveRecordAssociations` refines RBI files for subclasses of `ActiveRecord::Base`
|
16
|
+
# (see https://api.rubyonrails.org/classes/ActiveRecord/Base.html). This generator is only
|
17
|
+
# responsible for defining the methods that would be created for the association that
|
18
|
+
# are defined in the Active Record model.
|
19
|
+
#
|
20
|
+
# For example, with the following model class:
|
21
|
+
#
|
22
|
+
# ~~~rb
|
23
|
+
# class Post < ActiveRecord::Base
|
24
|
+
# belongs_to :category
|
25
|
+
# has_many :comments
|
26
|
+
# has_one :author, class_name: "User"
|
27
|
+
# end
|
28
|
+
# ~~~
|
29
|
+
#
|
30
|
+
# this generator will produce the following methods in the RBI file
|
31
|
+
# `post.rbi`:
|
32
|
+
#
|
33
|
+
# ~~~rbi
|
34
|
+
# # post.rbi
|
35
|
+
# # typed: true
|
36
|
+
#
|
37
|
+
# class Post
|
38
|
+
# include Post::GeneratedAssociationMethods
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# module Post::GeneratedAssociationMethods
|
42
|
+
# sig { returns(T.nilable(::User)) }
|
43
|
+
# def author; end
|
44
|
+
#
|
45
|
+
# sig { params(value: T.nilable(::User)).void }
|
46
|
+
# def author=(value); end
|
47
|
+
#
|
48
|
+
# sig { params(args: T.untyped, blk: T.untyped).returns(T.nilable(::User)) }
|
49
|
+
# def build_author(*args, &blk); end
|
50
|
+
#
|
51
|
+
# sig { params(args: T.untyped, blk: T.untyped).returns(T.nilable(::Category)) }
|
52
|
+
# def build_category(*args, &blk); end
|
53
|
+
#
|
54
|
+
# sig { returns(T.nilable(::Category)) }
|
55
|
+
# def category; end
|
56
|
+
#
|
57
|
+
# sig { params(value: T.nilable(::Category)).void }
|
58
|
+
# def category=(value); end
|
59
|
+
#
|
60
|
+
# sig { returns(T::Array[T.untyped]) }
|
61
|
+
# def comment_ids; end
|
62
|
+
#
|
63
|
+
# sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
|
64
|
+
# def comment_ids=(ids); end
|
65
|
+
#
|
66
|
+
# sig { returns(::ActiveRecord::Associations::CollectionProxy[Comment]) }
|
67
|
+
# def comments; end
|
68
|
+
#
|
69
|
+
# sig { params(value: T::Enumerable[::Comment]).void }
|
70
|
+
# def comments=(value); end
|
71
|
+
#
|
72
|
+
# sig { params(args: T.untyped, blk: T.untyped).returns(T.nilable(::User)) }
|
73
|
+
# def create_author(*args, &blk); end
|
74
|
+
#
|
75
|
+
# sig { params(args: T.untyped, blk: T.untyped).returns(T.nilable(::User)) }
|
76
|
+
# def create_author!(*args, &blk); end
|
77
|
+
#
|
78
|
+
# sig { params(args: T.untyped, blk: T.untyped).returns(T.nilable(::Category)) }
|
79
|
+
# def create_category(*args, &blk); end
|
80
|
+
#
|
81
|
+
# sig { params(args: T.untyped, blk: T.untyped).returns(T.nilable(::Category)) }
|
82
|
+
# def create_category!(*args, &blk); end
|
83
|
+
#
|
84
|
+
# sig { returns(T.nilable(::User)) }
|
85
|
+
# def reload_author; end
|
86
|
+
#
|
87
|
+
# sig { returns(T.nilable(::Category)) }
|
88
|
+
# def reload_category; end
|
89
|
+
# end
|
90
|
+
# ~~~
|
91
|
+
class ActiveRecordAssociations < Base
|
92
|
+
extend T::Sig
|
93
|
+
|
94
|
+
ReflectionType = T.type_alias do
|
95
|
+
T.any(::ActiveRecord::Reflection::ThroughReflection, ::ActiveRecord::Reflection::AssociationReflection)
|
96
|
+
end
|
97
|
+
|
98
|
+
sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(ActiveRecord::Base)).void }
|
99
|
+
def decorate(root, constant)
|
100
|
+
return if constant.reflections.empty?
|
101
|
+
|
102
|
+
module_name = "#{constant}::GeneratedAssociationMethods"
|
103
|
+
root.create_module(module_name) do |mod|
|
104
|
+
constant.reflections.each do |association_name, reflection|
|
105
|
+
if reflection.collection?
|
106
|
+
populate_collection_assoc_getter_setter(mod, constant, association_name, reflection)
|
107
|
+
else
|
108
|
+
populate_single_assoc_getter_setter(mod, constant, association_name, reflection)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
root.path(constant) do |klass|
|
114
|
+
klass.create_include(module_name)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
sig { override.returns(T::Enumerable[Module]) }
|
119
|
+
def gather_constants
|
120
|
+
ActiveRecord::Base.descendants.reject(&:abstract_class?)
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
sig do
|
126
|
+
params(
|
127
|
+
klass: Parlour::RbiGenerator::Namespace,
|
128
|
+
constant: T.class_of(ActiveRecord::Base),
|
129
|
+
association_name: T.any(String, Symbol),
|
130
|
+
reflection: ReflectionType
|
131
|
+
).void
|
132
|
+
end
|
133
|
+
def populate_single_assoc_getter_setter(klass, constant, association_name, reflection)
|
134
|
+
association_class = type_for(constant, reflection)
|
135
|
+
association_type = if belongs_to_and_required?(constant, reflection)
|
136
|
+
association_class
|
137
|
+
else
|
138
|
+
"T.nilable(#{association_class})"
|
139
|
+
end
|
140
|
+
|
141
|
+
create_method(
|
142
|
+
klass,
|
143
|
+
association_name.to_s,
|
144
|
+
return_type: association_type,
|
145
|
+
)
|
146
|
+
create_method(
|
147
|
+
klass,
|
148
|
+
"#{association_name}=",
|
149
|
+
parameters: [
|
150
|
+
Parlour::RbiGenerator::Parameter.new("value", type: association_type),
|
151
|
+
],
|
152
|
+
return_type: nil
|
153
|
+
)
|
154
|
+
create_method(
|
155
|
+
klass,
|
156
|
+
"reload_#{association_name}",
|
157
|
+
return_type: association_type,
|
158
|
+
)
|
159
|
+
if reflection.constructable?
|
160
|
+
create_method(
|
161
|
+
klass,
|
162
|
+
"build_#{association_name}",
|
163
|
+
parameters: [
|
164
|
+
Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped"),
|
165
|
+
Parlour::RbiGenerator::Parameter.new("&blk", type: "T.untyped"),
|
166
|
+
],
|
167
|
+
return_type: association_type
|
168
|
+
)
|
169
|
+
create_method(
|
170
|
+
klass,
|
171
|
+
"create_#{association_name}",
|
172
|
+
parameters: [
|
173
|
+
Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped"),
|
174
|
+
Parlour::RbiGenerator::Parameter.new("&blk", type: "T.untyped"),
|
175
|
+
],
|
176
|
+
return_type: association_type
|
177
|
+
)
|
178
|
+
create_method(
|
179
|
+
klass,
|
180
|
+
"create_#{association_name}!",
|
181
|
+
parameters: [
|
182
|
+
Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped"),
|
183
|
+
Parlour::RbiGenerator::Parameter.new("&blk", type: "T.untyped"),
|
184
|
+
],
|
185
|
+
return_type: association_type
|
186
|
+
)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
sig do
|
191
|
+
params(
|
192
|
+
klass: Parlour::RbiGenerator::Namespace,
|
193
|
+
constant: T.class_of(ActiveRecord::Base),
|
194
|
+
association_name: T.any(String, Symbol),
|
195
|
+
reflection: ReflectionType
|
196
|
+
).void
|
197
|
+
end
|
198
|
+
def populate_collection_assoc_getter_setter(klass, constant, association_name, reflection)
|
199
|
+
association_class = type_for(constant, reflection)
|
200
|
+
relation_class = relation_type_for(constant, reflection)
|
201
|
+
|
202
|
+
create_method(
|
203
|
+
klass,
|
204
|
+
association_name.to_s,
|
205
|
+
return_type: relation_class,
|
206
|
+
)
|
207
|
+
create_method(
|
208
|
+
klass,
|
209
|
+
"#{association_name}=",
|
210
|
+
parameters: [
|
211
|
+
Parlour::RbiGenerator::Parameter.new("value", type: "T::Enumerable[#{association_class}]"),
|
212
|
+
],
|
213
|
+
return_type: nil,
|
214
|
+
)
|
215
|
+
create_method(
|
216
|
+
klass,
|
217
|
+
"#{association_name.to_s.singularize}_ids",
|
218
|
+
return_type: "T::Array[T.untyped]"
|
219
|
+
)
|
220
|
+
create_method(
|
221
|
+
klass,
|
222
|
+
"#{association_name.to_s.singularize}_ids=",
|
223
|
+
parameters: [
|
224
|
+
Parlour::RbiGenerator::Parameter.new("ids", type: "T::Array[T.untyped]"),
|
225
|
+
],
|
226
|
+
return_type: "T::Array[T.untyped]"
|
227
|
+
)
|
228
|
+
end
|
229
|
+
|
230
|
+
sig do
|
231
|
+
params(
|
232
|
+
constant: T.class_of(ActiveRecord::Base),
|
233
|
+
reflection: ReflectionType
|
234
|
+
).returns(T::Boolean)
|
235
|
+
end
|
236
|
+
def belongs_to_and_required?(constant, reflection)
|
237
|
+
return false unless constant.table_exists?
|
238
|
+
return false unless reflection.belongs_to?
|
239
|
+
column_definition = constant.columns_hash[reflection.foreign_key.to_s]
|
240
|
+
|
241
|
+
!column_definition.nil? && !column_definition.null
|
242
|
+
end
|
243
|
+
|
244
|
+
sig do
|
245
|
+
params(
|
246
|
+
constant: T.class_of(ActiveRecord::Base),
|
247
|
+
reflection: ReflectionType
|
248
|
+
).returns(String)
|
249
|
+
end
|
250
|
+
def type_for(constant, reflection)
|
251
|
+
return "T.untyped" if !constant.table_exists? || polymorphic_association?(reflection)
|
252
|
+
|
253
|
+
"::#{reflection.klass.name}"
|
254
|
+
end
|
255
|
+
|
256
|
+
sig do
|
257
|
+
params(
|
258
|
+
constant: T.class_of(ActiveRecord::Base),
|
259
|
+
reflection: ReflectionType
|
260
|
+
).returns(String)
|
261
|
+
end
|
262
|
+
def relation_type_for(constant, reflection)
|
263
|
+
"ActiveRecord::Associations::CollectionProxy" if !constant.table_exists? ||
|
264
|
+
polymorphic_association?(reflection)
|
265
|
+
|
266
|
+
# Change to: "::#{reflection.klass.name}::ActiveRecord_Associations_CollectionProxy"
|
267
|
+
"::ActiveRecord::Associations::CollectionProxy[#{reflection.klass.name}]"
|
268
|
+
end
|
269
|
+
|
270
|
+
sig do
|
271
|
+
params(
|
272
|
+
reflection: ReflectionType
|
273
|
+
).returns(T::Boolean)
|
274
|
+
end
|
275
|
+
def polymorphic_association?(reflection)
|
276
|
+
if reflection.through_reflection?
|
277
|
+
polymorphic_association?(reflection.source_reflection)
|
278
|
+
else
|
279
|
+
!!reflection.polymorphic?
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
@@ -0,0 +1,379 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "parlour"
|
5
|
+
|
6
|
+
begin
|
7
|
+
require "active_record"
|
8
|
+
rescue LoadError
|
9
|
+
return
|
10
|
+
end
|
11
|
+
|
12
|
+
module Tapioca
|
13
|
+
module Compilers
|
14
|
+
module Dsl
|
15
|
+
# `Tapioca::Compilers::Dsl::ActiveRecordColumns` refines RBI files for subclasses of `ActiveRecord::Base`
|
16
|
+
# (see https://api.rubyonrails.org/classes/ActiveRecord/Base.html). This generator is only
|
17
|
+
# responsible for defining the attribute methods that would be created for the columns that
|
18
|
+
# are defined in the Active Record model.
|
19
|
+
#
|
20
|
+
# For example, with the following model class:
|
21
|
+
#
|
22
|
+
# ~~~rb
|
23
|
+
# class Post < ActiveRecord::Base
|
24
|
+
# end
|
25
|
+
# ~~~
|
26
|
+
#
|
27
|
+
# and the following database schema:
|
28
|
+
#
|
29
|
+
# ~~~rb
|
30
|
+
# # db/schema.rb
|
31
|
+
# create_table :posts do |t|
|
32
|
+
# t.string :title, null: false
|
33
|
+
# t.string :body
|
34
|
+
# t.boolean :published
|
35
|
+
# t.timestamps
|
36
|
+
# end
|
37
|
+
# ~~~
|
38
|
+
#
|
39
|
+
# this generator will produce the following methods in the RBI file
|
40
|
+
# `post.rbi`:
|
41
|
+
#
|
42
|
+
# ~~~rbi
|
43
|
+
# # post.rbi
|
44
|
+
# # typed: true
|
45
|
+
# class Post
|
46
|
+
# sig { returns(T.nilable(::String)) }
|
47
|
+
# def body; end
|
48
|
+
#
|
49
|
+
# sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
|
50
|
+
# def body=; end
|
51
|
+
#
|
52
|
+
# sig { params(args: T.untyped).returns(T::Boolean) }
|
53
|
+
# def body?; end
|
54
|
+
#
|
55
|
+
# sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
|
56
|
+
# def created_at; end
|
57
|
+
#
|
58
|
+
# sig { params(value: ::ActiveSupport::TimeWithZone).returns(::ActiveSupport::TimeWithZone) }
|
59
|
+
# def created_at=; end
|
60
|
+
#
|
61
|
+
# sig { params(args: T.untyped).returns(T::Boolean) }
|
62
|
+
# def created_at?; end
|
63
|
+
#
|
64
|
+
# sig { returns(T.nilable(T::Boolean)) }
|
65
|
+
# def published; end
|
66
|
+
#
|
67
|
+
# sig { params(value: T::Boolean).returns(T::Boolean) }
|
68
|
+
# def published=; end
|
69
|
+
#
|
70
|
+
# sig { params(args: T.untyped).returns(T::Boolean) }
|
71
|
+
# def published?; end
|
72
|
+
#
|
73
|
+
# sig { returns(::String) }
|
74
|
+
# def title; end
|
75
|
+
#
|
76
|
+
# sig { params(value: ::String).returns(::String) }
|
77
|
+
# def title=(value); end
|
78
|
+
#
|
79
|
+
# sig { params(args: T.untyped).returns(T::Boolean) }
|
80
|
+
# def title?(*args); end
|
81
|
+
#
|
82
|
+
# sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
|
83
|
+
# def updated_at; end
|
84
|
+
#
|
85
|
+
# sig { params(value: ::ActiveSupport::TimeWithZone).returns(::ActiveSupport::TimeWithZone) }
|
86
|
+
# def updated_at=; end
|
87
|
+
#
|
88
|
+
# sig { params(args: T.untyped).returns(T::Boolean) }
|
89
|
+
# def updated_at?; end
|
90
|
+
#
|
91
|
+
# ## Also the methods added by https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Dirty.html
|
92
|
+
# ## Also the methods added by https://api.rubyonrails.org/classes/ActiveModel/Dirty.html
|
93
|
+
# ## Also the methods added by https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/BeforeTypeCast.html
|
94
|
+
# end
|
95
|
+
# ~~~
|
96
|
+
class ActiveRecordColumns < Base
|
97
|
+
extend T::Sig
|
98
|
+
|
99
|
+
sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(ActiveRecord::Base)).void }
|
100
|
+
def decorate(root, constant)
|
101
|
+
return unless constant.table_exists?
|
102
|
+
|
103
|
+
module_name = "#{constant}::GeneratedAttributeMethods"
|
104
|
+
root.create_module(module_name) do |mod|
|
105
|
+
constant.columns_hash.each_key do |column_name|
|
106
|
+
column_name = column_name.to_s
|
107
|
+
add_methods_for_attribute(mod, constant, column_name)
|
108
|
+
end
|
109
|
+
|
110
|
+
constant.attribute_aliases.each do |attribute_name, column_name|
|
111
|
+
attribute_name = attribute_name.to_s
|
112
|
+
column_name = column_name.to_s
|
113
|
+
new_method_names = constant.attribute_method_matchers.map { |m| m.method_name(attribute_name) }
|
114
|
+
old_method_names = constant.attribute_method_matchers.map { |m| m.method_name(column_name) }
|
115
|
+
methods_to_add = new_method_names - old_method_names
|
116
|
+
|
117
|
+
add_methods_for_attribute(mod, constant, column_name, attribute_name, methods_to_add)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
root.path(constant) do |klass|
|
122
|
+
klass.create_include(module_name)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
sig { override.returns(T::Enumerable[Module]) }
|
127
|
+
def gather_constants
|
128
|
+
ActiveRecord::Base.descendants.reject(&:abstract_class?)
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
sig do
|
134
|
+
params(
|
135
|
+
klass: Parlour::RbiGenerator::Namespace,
|
136
|
+
name: String,
|
137
|
+
methods_to_add: T.nilable(T::Array[String]),
|
138
|
+
return_type: T.nilable(String),
|
139
|
+
parameters: T::Array[[String, String]]
|
140
|
+
).void
|
141
|
+
end
|
142
|
+
def add_method(klass, name, methods_to_add, return_type: nil, parameters: [])
|
143
|
+
create_method(
|
144
|
+
klass,
|
145
|
+
name,
|
146
|
+
parameters: parameters.map do |param, type|
|
147
|
+
Parlour::RbiGenerator::Parameter.new(param, type: type)
|
148
|
+
end,
|
149
|
+
return_type: return_type
|
150
|
+
) if methods_to_add.nil? || methods_to_add.include?(name)
|
151
|
+
end
|
152
|
+
|
153
|
+
sig do
|
154
|
+
params(
|
155
|
+
klass: Parlour::RbiGenerator::Namespace,
|
156
|
+
constant: T.class_of(ActiveRecord::Base),
|
157
|
+
column_name: String,
|
158
|
+
attribute_name: String,
|
159
|
+
methods_to_add: T.nilable(T::Array[String])
|
160
|
+
).void
|
161
|
+
end
|
162
|
+
def add_methods_for_attribute(klass, constant, column_name, attribute_name = column_name, methods_to_add = nil)
|
163
|
+
getter_type, setter_type = type_for(constant, column_name)
|
164
|
+
|
165
|
+
# Added by ActiveRecord::AttributeMethods::Read
|
166
|
+
#
|
167
|
+
add_method(
|
168
|
+
klass,
|
169
|
+
attribute_name.to_s,
|
170
|
+
methods_to_add,
|
171
|
+
return_type: getter_type
|
172
|
+
)
|
173
|
+
|
174
|
+
# Added by ActiveRecord::AttributeMethods::Write
|
175
|
+
#
|
176
|
+
add_method(
|
177
|
+
klass,
|
178
|
+
"#{attribute_name}=",
|
179
|
+
methods_to_add,
|
180
|
+
parameters: [["value", setter_type]],
|
181
|
+
return_type: setter_type
|
182
|
+
)
|
183
|
+
|
184
|
+
# Added by ActiveRecord::AttributeMethods::Query
|
185
|
+
#
|
186
|
+
add_method(
|
187
|
+
klass,
|
188
|
+
"#{attribute_name}?",
|
189
|
+
methods_to_add,
|
190
|
+
return_type: "T::Boolean"
|
191
|
+
)
|
192
|
+
|
193
|
+
# Added by ActiveRecord::AttributeMethods::Dirty
|
194
|
+
#
|
195
|
+
add_method(
|
196
|
+
klass,
|
197
|
+
"#{attribute_name}_before_last_save",
|
198
|
+
methods_to_add,
|
199
|
+
return_type: getter_type
|
200
|
+
)
|
201
|
+
add_method(
|
202
|
+
klass,
|
203
|
+
"#{attribute_name}_change_to_be_saved",
|
204
|
+
methods_to_add,
|
205
|
+
return_type: "[#{getter_type}, #{getter_type}]"
|
206
|
+
)
|
207
|
+
add_method(
|
208
|
+
klass,
|
209
|
+
"#{attribute_name}_in_database",
|
210
|
+
methods_to_add,
|
211
|
+
return_type: getter_type
|
212
|
+
)
|
213
|
+
add_method(
|
214
|
+
klass,
|
215
|
+
"saved_change_to_#{attribute_name}",
|
216
|
+
methods_to_add,
|
217
|
+
return_type: "[#{getter_type}, #{getter_type}]"
|
218
|
+
)
|
219
|
+
add_method(
|
220
|
+
klass,
|
221
|
+
"saved_change_to_#{attribute_name}?",
|
222
|
+
methods_to_add,
|
223
|
+
return_type: "T::Boolean"
|
224
|
+
)
|
225
|
+
add_method(
|
226
|
+
klass,
|
227
|
+
"will_save_change_to_#{attribute_name}?",
|
228
|
+
methods_to_add,
|
229
|
+
return_type: "T::Boolean"
|
230
|
+
)
|
231
|
+
|
232
|
+
# Added by ActiveModel::Dirty
|
233
|
+
#
|
234
|
+
add_method(
|
235
|
+
klass,
|
236
|
+
"#{attribute_name}_change",
|
237
|
+
methods_to_add,
|
238
|
+
return_type: "[#{getter_type}, #{getter_type}]"
|
239
|
+
)
|
240
|
+
add_method(
|
241
|
+
klass,
|
242
|
+
"#{attribute_name}_changed?",
|
243
|
+
methods_to_add,
|
244
|
+
return_type: "T::Boolean"
|
245
|
+
)
|
246
|
+
add_method(
|
247
|
+
klass,
|
248
|
+
"#{attribute_name}_will_change!",
|
249
|
+
methods_to_add
|
250
|
+
)
|
251
|
+
add_method(
|
252
|
+
klass,
|
253
|
+
"#{attribute_name}_was",
|
254
|
+
methods_to_add,
|
255
|
+
return_type: getter_type
|
256
|
+
)
|
257
|
+
add_method(
|
258
|
+
klass,
|
259
|
+
"#{attribute_name}_previous_change",
|
260
|
+
methods_to_add,
|
261
|
+
return_type: "[#{getter_type}, #{getter_type}]"
|
262
|
+
)
|
263
|
+
add_method(
|
264
|
+
klass,
|
265
|
+
"#{attribute_name}_previously_changed?",
|
266
|
+
methods_to_add,
|
267
|
+
return_type: "T::Boolean"
|
268
|
+
)
|
269
|
+
add_method(
|
270
|
+
klass,
|
271
|
+
"#{attribute_name}_previously_was",
|
272
|
+
methods_to_add,
|
273
|
+
return_type: getter_type
|
274
|
+
)
|
275
|
+
add_method(
|
276
|
+
klass,
|
277
|
+
"restore_#{attribute_name}!",
|
278
|
+
methods_to_add
|
279
|
+
)
|
280
|
+
|
281
|
+
# Added by ActiveRecord::AttributeMethods::BeforeTypeCast
|
282
|
+
#
|
283
|
+
add_method(
|
284
|
+
klass,
|
285
|
+
"#{attribute_name}_before_type_cast",
|
286
|
+
methods_to_add,
|
287
|
+
return_type: "T.untyped"
|
288
|
+
)
|
289
|
+
add_method(
|
290
|
+
klass,
|
291
|
+
"#{attribute_name}_came_from_user?",
|
292
|
+
methods_to_add,
|
293
|
+
return_type: "T::Boolean"
|
294
|
+
)
|
295
|
+
end
|
296
|
+
|
297
|
+
sig do
|
298
|
+
params(
|
299
|
+
constant: T.class_of(ActiveRecord::Base),
|
300
|
+
column_name: String
|
301
|
+
).returns([String, String])
|
302
|
+
end
|
303
|
+
def type_for(constant, column_name)
|
304
|
+
return ["T.untyped", "T.untyped"] if do_not_generate_strong_types?(constant)
|
305
|
+
|
306
|
+
column_type = constant.attribute_types[column_name]
|
307
|
+
|
308
|
+
getter_type =
|
309
|
+
case column_type
|
310
|
+
when ActiveRecord::Type::Integer
|
311
|
+
"::Integer"
|
312
|
+
when ActiveRecord::Type::String
|
313
|
+
"::String"
|
314
|
+
when ActiveRecord::Type::Date
|
315
|
+
"::Date"
|
316
|
+
when ActiveRecord::Type::Decimal
|
317
|
+
"::BigDecimal"
|
318
|
+
when ActiveRecord::Type::Float
|
319
|
+
"::Float"
|
320
|
+
when ActiveRecord::Type::Boolean
|
321
|
+
"T::Boolean"
|
322
|
+
when ActiveRecord::Type::DateTime, ActiveRecord::Type::Time
|
323
|
+
"::DateTime"
|
324
|
+
when ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter
|
325
|
+
"::ActiveSupport::TimeWithZone"
|
326
|
+
else
|
327
|
+
handle_unknown_type(column_type)
|
328
|
+
end
|
329
|
+
|
330
|
+
column = constant.columns_hash[column_name]
|
331
|
+
setter_type = getter_type
|
332
|
+
|
333
|
+
if column&.null
|
334
|
+
return ["T.nilable(#{getter_type})", "T.nilable(#{setter_type})"]
|
335
|
+
end
|
336
|
+
|
337
|
+
if column_name == constant.primary_key ||
|
338
|
+
column_name == "created_at" ||
|
339
|
+
column_name == "updated_at"
|
340
|
+
getter_type = "T.nilable(#{getter_type})"
|
341
|
+
end
|
342
|
+
|
343
|
+
[getter_type, setter_type]
|
344
|
+
end
|
345
|
+
|
346
|
+
sig { params(constant: Module).returns(T::Boolean) }
|
347
|
+
def do_not_generate_strong_types?(constant)
|
348
|
+
Object.const_defined?(:StrongTypeGeneration) &&
|
349
|
+
!(constant.singleton_class < Object.const_get(:StrongTypeGeneration))
|
350
|
+
end
|
351
|
+
|
352
|
+
sig { params(column_type: Module).returns(String) }
|
353
|
+
def handle_unknown_type(column_type)
|
354
|
+
return "T.untyped" unless column_type < ActiveModel::Type::Value
|
355
|
+
|
356
|
+
lookup_return_type_of_method(column_type, :deserialize) ||
|
357
|
+
lookup_return_type_of_method(column_type, :cast) ||
|
358
|
+
lookup_arg_type_of_method(column_type, :serialize) ||
|
359
|
+
"T.untyped"
|
360
|
+
end
|
361
|
+
|
362
|
+
sig { params(column_type: Module, method: Symbol).returns(T.nilable(String)) }
|
363
|
+
def lookup_return_type_of_method(column_type, method)
|
364
|
+
signature = T::Private::Methods.signature_for_method(column_type.instance_method(method))
|
365
|
+
return unless signature
|
366
|
+
|
367
|
+
return_type = signature.return_type.to_s
|
368
|
+
return_type if return_type != "<VOID>" && return_type != "<NOT-TYPED>"
|
369
|
+
end
|
370
|
+
|
371
|
+
sig { params(column_type: Module, method: Symbol).returns(T.nilable(String)) }
|
372
|
+
def lookup_arg_type_of_method(column_type, method)
|
373
|
+
signature = T::Private::Methods.signature_for_method(column_type.instance_method(method))
|
374
|
+
signature.arg_types.first.last.to_s if signature
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|