tapioca 0.4.0 → 0.4.5

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +26 -1
  3. data/README.md +16 -0
  4. data/Rakefile +16 -4
  5. data/lib/tapioca.rb +6 -2
  6. data/lib/tapioca/cli.rb +25 -3
  7. data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +130 -0
  8. data/lib/tapioca/compilers/dsl/action_mailer.rb +65 -0
  9. data/lib/tapioca/compilers/dsl/active_record_associations.rb +267 -0
  10. data/lib/tapioca/compilers/dsl/active_record_columns.rb +404 -0
  11. data/lib/tapioca/compilers/dsl/active_record_enum.rb +112 -0
  12. data/lib/tapioca/compilers/dsl/active_record_identity_cache.rb +212 -0
  13. data/lib/tapioca/compilers/dsl/active_record_scope.rb +100 -0
  14. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +168 -0
  15. data/lib/tapioca/compilers/dsl/active_resource.rb +140 -0
  16. data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +126 -0
  17. data/lib/tapioca/compilers/dsl/base.rb +165 -0
  18. data/lib/tapioca/compilers/dsl/frozen_record.rb +96 -0
  19. data/lib/tapioca/compilers/dsl/protobuf.rb +144 -0
  20. data/lib/tapioca/compilers/dsl/smart_properties.rb +173 -0
  21. data/lib/tapioca/compilers/dsl/state_machines.rb +378 -0
  22. data/lib/tapioca/compilers/dsl/url_helpers.rb +160 -0
  23. data/lib/tapioca/compilers/dsl_compiler.rb +121 -0
  24. data/lib/tapioca/compilers/requires_compiler.rb +67 -0
  25. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +195 -32
  26. data/lib/tapioca/config.rb +11 -6
  27. data/lib/tapioca/config_builder.rb +19 -9
  28. data/lib/tapioca/constant_locator.rb +1 -0
  29. data/lib/tapioca/core_ext/class.rb +23 -0
  30. data/lib/tapioca/gemfile.rb +32 -9
  31. data/lib/tapioca/generator.rb +200 -24
  32. data/lib/tapioca/loader.rb +30 -9
  33. data/lib/tapioca/version.rb +1 -1
  34. metadata +31 -40
@@ -0,0 +1,404 @@
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
+ # **Note:** This generator, by default, generates weak signatures for column methods and treats each
21
+ # column to be `T.untyped`. This is done on purpose to ensure that the nilability of Active Record
22
+ # columns do not make it hard for existing code to adopt gradual typing. It is possible, however, to
23
+ # generate stricter type signatures for your ActiveRecord column types. If your ActiveRecord model extends
24
+ # a module with name `StrongTypeGeneration`, this generator will generate stricter signatures that follow
25
+ # closely with the types defined in the schema.
26
+ #
27
+ # The `StrongTypeGeneration` module you define in your application should add an `after_initialize` callback
28
+ # to the model and ensure that all the non-nilable attributes of the model are actually initialized with non-`nil`
29
+ # values.
30
+ #
31
+ # For example, with the following model class:
32
+ #
33
+ # ~~~rb
34
+ # class Post < ActiveRecord::Base
35
+ # end
36
+ # ~~~
37
+ #
38
+ # and the following database schema:
39
+ #
40
+ # ~~~rb
41
+ # # db/schema.rb
42
+ # create_table :posts do |t|
43
+ # t.string :title, null: false
44
+ # t.string :body
45
+ # t.boolean :published
46
+ # t.timestamps
47
+ # end
48
+ # ~~~
49
+ #
50
+ # this generator will produce the following methods in the RBI file
51
+ # `post.rbi`:
52
+ #
53
+ # ~~~rbi
54
+ # # post.rbi
55
+ # # typed: true
56
+ # class Post
57
+ # sig { returns(T.nilable(::String)) }
58
+ # def body; end
59
+ #
60
+ # sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
61
+ # def body=; end
62
+ #
63
+ # sig { params(args: T.untyped).returns(T::Boolean) }
64
+ # def body?; end
65
+ #
66
+ # sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
67
+ # def created_at; end
68
+ #
69
+ # sig { params(value: ::ActiveSupport::TimeWithZone).returns(::ActiveSupport::TimeWithZone) }
70
+ # def created_at=; end
71
+ #
72
+ # sig { params(args: T.untyped).returns(T::Boolean) }
73
+ # def created_at?; end
74
+ #
75
+ # sig { returns(T.nilable(T::Boolean)) }
76
+ # def published; end
77
+ #
78
+ # sig { params(value: T::Boolean).returns(T::Boolean) }
79
+ # def published=; end
80
+ #
81
+ # sig { params(args: T.untyped).returns(T::Boolean) }
82
+ # def published?; end
83
+ #
84
+ # sig { returns(::String) }
85
+ # def title; end
86
+ #
87
+ # sig { params(value: ::String).returns(::String) }
88
+ # def title=(value); end
89
+ #
90
+ # sig { params(args: T.untyped).returns(T::Boolean) }
91
+ # def title?(*args); end
92
+ #
93
+ # sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
94
+ # def updated_at; end
95
+ #
96
+ # sig { params(value: ::ActiveSupport::TimeWithZone).returns(::ActiveSupport::TimeWithZone) }
97
+ # def updated_at=; end
98
+ #
99
+ # sig { params(args: T.untyped).returns(T::Boolean) }
100
+ # def updated_at?; end
101
+ #
102
+ # ## Also the methods added by https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Dirty.html
103
+ # ## Also the methods added by https://api.rubyonrails.org/classes/ActiveModel/Dirty.html
104
+ # ## Also the methods added by https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/BeforeTypeCast.html
105
+ # end
106
+ # ~~~
107
+ class ActiveRecordColumns < Base
108
+ extend T::Sig
109
+
110
+ sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(ActiveRecord::Base)).void }
111
+ def decorate(root, constant)
112
+ return unless constant.table_exists?
113
+
114
+ module_name = "#{constant}::GeneratedAttributeMethods"
115
+ root.create_module(module_name) do |mod|
116
+ constant.columns_hash.each_key do |column_name|
117
+ column_name = column_name.to_s
118
+ add_methods_for_attribute(mod, constant, column_name)
119
+ end
120
+
121
+ constant.attribute_aliases.each do |attribute_name, column_name|
122
+ attribute_name = attribute_name.to_s
123
+ column_name = column_name.to_s
124
+ new_method_names = constant.attribute_method_matchers.map { |m| m.method_name(attribute_name) }
125
+ old_method_names = constant.attribute_method_matchers.map { |m| m.method_name(column_name) }
126
+ methods_to_add = new_method_names - old_method_names
127
+
128
+ add_methods_for_attribute(mod, constant, column_name, attribute_name, methods_to_add)
129
+ end
130
+ end
131
+
132
+ root.path(constant) do |klass|
133
+ klass.create_include(module_name)
134
+ end
135
+ end
136
+
137
+ sig { override.returns(T::Enumerable[Module]) }
138
+ def gather_constants
139
+ ActiveRecord::Base.descendants.reject(&:abstract_class?)
140
+ end
141
+
142
+ private
143
+
144
+ sig do
145
+ params(
146
+ klass: Parlour::RbiGenerator::Namespace,
147
+ name: String,
148
+ methods_to_add: T.nilable(T::Array[String]),
149
+ return_type: T.nilable(String),
150
+ parameters: T::Array[[String, String]]
151
+ ).void
152
+ end
153
+ def add_method(klass, name, methods_to_add, return_type: nil, parameters: [])
154
+ create_method(
155
+ klass,
156
+ name,
157
+ parameters: parameters.map do |param, type|
158
+ Parlour::RbiGenerator::Parameter.new(param, type: type)
159
+ end,
160
+ return_type: return_type
161
+ ) if methods_to_add.nil? || methods_to_add.include?(name)
162
+ end
163
+
164
+ sig do
165
+ params(
166
+ klass: Parlour::RbiGenerator::Namespace,
167
+ constant: T.class_of(ActiveRecord::Base),
168
+ column_name: String,
169
+ attribute_name: String,
170
+ methods_to_add: T.nilable(T::Array[String])
171
+ ).void
172
+ end
173
+ def add_methods_for_attribute(klass, constant, column_name, attribute_name = column_name, methods_to_add = nil)
174
+ getter_type, setter_type = type_for(constant, column_name)
175
+
176
+ # Added by ActiveRecord::AttributeMethods::Read
177
+ #
178
+ add_method(
179
+ klass,
180
+ attribute_name.to_s,
181
+ methods_to_add,
182
+ return_type: getter_type
183
+ )
184
+
185
+ # Added by ActiveRecord::AttributeMethods::Write
186
+ #
187
+ add_method(
188
+ klass,
189
+ "#{attribute_name}=",
190
+ methods_to_add,
191
+ parameters: [["value", setter_type]],
192
+ return_type: setter_type
193
+ )
194
+
195
+ # Added by ActiveRecord::AttributeMethods::Query
196
+ #
197
+ add_method(
198
+ klass,
199
+ "#{attribute_name}?",
200
+ methods_to_add,
201
+ return_type: "T::Boolean"
202
+ )
203
+
204
+ # Added by ActiveRecord::AttributeMethods::Dirty
205
+ #
206
+ add_method(
207
+ klass,
208
+ "#{attribute_name}_before_last_save",
209
+ methods_to_add,
210
+ return_type: as_nilable_type(getter_type)
211
+ )
212
+ add_method(
213
+ klass,
214
+ "#{attribute_name}_change_to_be_saved",
215
+ methods_to_add,
216
+ return_type: "T.nilable([#{getter_type}, #{getter_type}])"
217
+ )
218
+ add_method(
219
+ klass,
220
+ "#{attribute_name}_in_database",
221
+ methods_to_add,
222
+ return_type: as_nilable_type(getter_type)
223
+ )
224
+ add_method(
225
+ klass,
226
+ "saved_change_to_#{attribute_name}",
227
+ methods_to_add,
228
+ return_type: "T.nilable([#{getter_type}, #{getter_type}])"
229
+ )
230
+ add_method(
231
+ klass,
232
+ "saved_change_to_#{attribute_name}?",
233
+ methods_to_add,
234
+ return_type: "T::Boolean"
235
+ )
236
+ add_method(
237
+ klass,
238
+ "will_save_change_to_#{attribute_name}?",
239
+ methods_to_add,
240
+ return_type: "T::Boolean"
241
+ )
242
+
243
+ # Added by ActiveModel::Dirty
244
+ #
245
+ add_method(
246
+ klass,
247
+ "#{attribute_name}_change",
248
+ methods_to_add,
249
+ return_type: "T.nilable([#{getter_type}, #{getter_type}])"
250
+ )
251
+ add_method(
252
+ klass,
253
+ "#{attribute_name}_changed?",
254
+ methods_to_add,
255
+ return_type: "T::Boolean"
256
+ )
257
+ add_method(
258
+ klass,
259
+ "#{attribute_name}_will_change!",
260
+ methods_to_add
261
+ )
262
+ add_method(
263
+ klass,
264
+ "#{attribute_name}_was",
265
+ methods_to_add,
266
+ return_type: as_nilable_type(getter_type)
267
+ )
268
+ add_method(
269
+ klass,
270
+ "#{attribute_name}_previous_change",
271
+ methods_to_add,
272
+ return_type: "T.nilable([#{getter_type}, #{getter_type}])"
273
+ )
274
+ add_method(
275
+ klass,
276
+ "#{attribute_name}_previously_changed?",
277
+ methods_to_add,
278
+ return_type: "T::Boolean"
279
+ )
280
+ add_method(
281
+ klass,
282
+ "#{attribute_name}_previously_was",
283
+ methods_to_add,
284
+ return_type: as_nilable_type(getter_type)
285
+ )
286
+ add_method(
287
+ klass,
288
+ "restore_#{attribute_name}!",
289
+ methods_to_add
290
+ )
291
+
292
+ # Added by ActiveRecord::AttributeMethods::BeforeTypeCast
293
+ #
294
+ add_method(
295
+ klass,
296
+ "#{attribute_name}_before_type_cast",
297
+ methods_to_add,
298
+ return_type: "T.untyped"
299
+ )
300
+ add_method(
301
+ klass,
302
+ "#{attribute_name}_came_from_user?",
303
+ methods_to_add,
304
+ return_type: "T::Boolean"
305
+ )
306
+ end
307
+
308
+ sig do
309
+ params(
310
+ constant: T.class_of(ActiveRecord::Base),
311
+ column_name: String
312
+ ).returns([String, String])
313
+ end
314
+ def type_for(constant, column_name)
315
+ return ["T.untyped", "T.untyped"] if do_not_generate_strong_types?(constant)
316
+
317
+ column_type = constant.attribute_types[column_name]
318
+
319
+ getter_type =
320
+ case column_type
321
+ when ActiveRecord::Type::Integer
322
+ "::Integer"
323
+ when ActiveRecord::Type::String
324
+ "::String"
325
+ when ActiveRecord::Type::Date
326
+ "::Date"
327
+ when ActiveRecord::Type::Decimal
328
+ "::BigDecimal"
329
+ when ActiveRecord::Type::Float
330
+ "::Float"
331
+ when ActiveRecord::Type::Boolean
332
+ "T::Boolean"
333
+ when ActiveRecord::Type::DateTime, ActiveRecord::Type::Time
334
+ "::DateTime"
335
+ when ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter
336
+ "::ActiveSupport::TimeWithZone"
337
+ else
338
+ handle_unknown_type(column_type)
339
+ end
340
+
341
+ column = constant.columns_hash[column_name]
342
+ setter_type = getter_type
343
+
344
+ if column&.null
345
+ return ["T.nilable(#{getter_type})", "T.nilable(#{setter_type})"]
346
+ end
347
+
348
+ if column_name == constant.primary_key ||
349
+ column_name == "created_at" ||
350
+ column_name == "updated_at"
351
+ getter_type = "T.nilable(#{getter_type})"
352
+ end
353
+
354
+ [getter_type, setter_type]
355
+ end
356
+
357
+ sig { params(constant: Module).returns(T::Boolean) }
358
+ def do_not_generate_strong_types?(constant)
359
+ Object.const_defined?(:StrongTypeGeneration) &&
360
+ !(constant.singleton_class < Object.const_get(:StrongTypeGeneration))
361
+ end
362
+
363
+ sig { params(column_type: Object).returns(String) }
364
+ def handle_unknown_type(column_type)
365
+ return "T.untyped" unless ActiveModel::Type::Value === column_type
366
+
367
+ lookup_return_type_of_method(column_type, :deserialize) ||
368
+ lookup_return_type_of_method(column_type, :cast) ||
369
+ lookup_arg_type_of_method(column_type, :serialize) ||
370
+ "T.untyped"
371
+ end
372
+
373
+ sig { params(column_type: ActiveModel::Type::Value, method: Symbol).returns(T.nilable(String)) }
374
+ def lookup_return_type_of_method(column_type, method)
375
+ signature = T::Private::Methods.signature_for_method(column_type.method(method))
376
+ return unless signature
377
+
378
+ return_type = signature.return_type
379
+ return if T::Types::Simple === return_type && T::Generic === return_type.raw_type
380
+ return if return_type == T::Private::Types::Void || return_type == T::Private::Types::NotTyped
381
+
382
+ return_type.to_s
383
+ end
384
+
385
+ sig { params(column_type: ActiveModel::Type::Value, method: Symbol).returns(T.nilable(String)) }
386
+ def lookup_arg_type_of_method(column_type, method)
387
+ signature = T::Private::Methods.signature_for_method(column_type.method(method))
388
+ return unless signature
389
+
390
+ arg_type = signature.arg_types.first.last
391
+ return if T::Types::Simple === arg_type && T::Generic === arg_type.raw_type
392
+
393
+ arg_type.to_s
394
+ end
395
+
396
+ sig { params(type: String).returns(String) }
397
+ def as_nilable_type(type)
398
+ return type if type.start_with?("T.nilable(")
399
+ "T.nilable(#{type})"
400
+ end
401
+ end
402
+ end
403
+ end
404
+ end
@@ -0,0 +1,112 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+
6
+ begin
7
+ require "active_record"
8
+ rescue LoadError
9
+ # means ActiveRecord is not installed,
10
+ # so let's not even define the generator.
11
+ return
12
+ end
13
+
14
+ module Tapioca
15
+ module Compilers
16
+ module Dsl
17
+ # `Tapioca::Compilers::Dsl::ActiveRecordEnum` decorates RBI files for subclasses of
18
+ # `ActiveRecord::Base` which declare `enum` fields
19
+ # (see https://api.rubyonrails.org/classes/ActiveRecord/Enum.html).
20
+ #
21
+ # For example, with the following `ActiveRecord::Base` subclass:
22
+ #
23
+ # ~~~rb
24
+ # class Post < ApplicationRecord
25
+ # enum title_type: %i(book all web), _suffix: :title
26
+ # end
27
+ # ~~~
28
+ #
29
+ # this generator will produce the RBI file `post.rbi` with the following content:
30
+ #
31
+ # ~~~rbi
32
+ # # post.rbi
33
+ # # typed: true
34
+ # class Post
35
+ # sig { void }
36
+ # def all_title!; end
37
+ #
38
+ # sig { returns(T::Boolean) }
39
+ # def all_title?; end
40
+ #
41
+ # sig { returns(T::Hash[T.any(String, Symbol), Integer]) }
42
+ # def self.title_types; end
43
+ #
44
+ # sig { void }
45
+ # def book_title!; end
46
+ #
47
+ # sig { returns(T::Boolean) }
48
+ # def book_title?; end
49
+ #
50
+ # sig { void }
51
+ # def web_title!; end
52
+ #
53
+ # sig { returns(T::Boolean) }
54
+ # def web_title?; end
55
+ # end
56
+ # ~~~
57
+ class ActiveRecordEnum < Base
58
+ extend T::Sig
59
+
60
+ sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(::ActiveRecord::Base)).void }
61
+ def decorate(root, constant)
62
+ return if constant.defined_enums.empty?
63
+
64
+ module_name = "#{constant}::EnumMethodsModule"
65
+ root.create_module(module_name) do |mod|
66
+ generate_instance_methods(constant, mod)
67
+ end
68
+
69
+ root.path(constant) do |k|
70
+ k.create_include(module_name)
71
+
72
+ constant.defined_enums.each do |name, enum_map|
73
+ type = type_for_enum(enum_map)
74
+ create_method(k, name.pluralize, class_method: true, return_type: type)
75
+ end
76
+ end
77
+ end
78
+
79
+ sig { override.returns(T::Enumerable[Module]) }
80
+ def gather_constants
81
+ ::ActiveRecord::Base.descendants.reject(&:abstract_class?)
82
+ end
83
+
84
+ private
85
+
86
+ sig { params(enum_map: T::Hash[T.untyped, T.untyped]).returns(String) }
87
+ def type_for_enum(enum_map)
88
+ value_type = enum_map.values.map { |v| v.class.name }.uniq
89
+ value_type = if value_type.length == 1
90
+ value_type.first
91
+ else
92
+ "T.any(#{value_type.join(', ')})"
93
+ end
94
+
95
+ "T::Hash[T.any(String, Symbol), #{value_type}]"
96
+ end
97
+
98
+ sig { params(constant: T.class_of(::ActiveRecord::Base), klass: Parlour::RbiGenerator::Namespace).void }
99
+ def generate_instance_methods(constant, klass)
100
+ methods = constant.send(:_enum_methods_module).instance_methods
101
+
102
+ methods.each do |method|
103
+ method = method.to_s
104
+ return_type = "T::Boolean" if method.end_with?("?")
105
+
106
+ create_method(klass, method, return_type: return_type)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end