tapioca 0.4.0 → 0.4.1

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 +25 -1
  3. data/README.md +12 -0
  4. data/Rakefile +15 -4
  5. data/lib/tapioca.rb +2 -0
  6. data/lib/tapioca/cli.rb +24 -2
  7. data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +129 -0
  8. data/lib/tapioca/compilers/dsl/action_mailer.rb +65 -0
  9. data/lib/tapioca/compilers/dsl/active_record_associations.rb +285 -0
  10. data/lib/tapioca/compilers/dsl/active_record_columns.rb +379 -0
  11. data/lib/tapioca/compilers/dsl/active_record_enum.rb +112 -0
  12. data/lib/tapioca/compilers/dsl/active_record_identity_cache.rb +213 -0
  13. data/lib/tapioca/compilers/dsl/active_record_scope.rb +100 -0
  14. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +170 -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 +163 -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 +83 -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 +141 -24
  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/generator.rb +187 -21
  31. data/lib/tapioca/loader.rb +20 -9
  32. data/lib/tapioca/sorbet_config_parser.rb +77 -0
  33. data/lib/tapioca/version.rb +1 -1
  34. metadata +29 -51
@@ -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
@@ -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