tapioca 0.4.0 → 0.4.5

Sign up to get free protection for your applications and to get access to all the features.
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