tapioca 0.3.1 → 0.4.4

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +25 -1
  3. data/README.md +23 -2
  4. data/Rakefile +15 -4
  5. data/lib/tapioca.rb +8 -2
  6. data/lib/tapioca/cli.rb +32 -3
  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 +267 -0
  10. data/lib/tapioca/compilers/dsl/active_record_columns.rb +393 -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 +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 +92 -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/sorbet.rb +34 -0
  26. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +171 -26
  27. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +1 -20
  28. data/lib/tapioca/compilers/todos_compiler.rb +32 -0
  29. data/lib/tapioca/config.rb +14 -6
  30. data/lib/tapioca/config_builder.rb +22 -9
  31. data/lib/tapioca/constant_locator.rb +1 -0
  32. data/lib/tapioca/core_ext/class.rb +23 -0
  33. data/lib/tapioca/gemfile.rb +32 -9
  34. data/lib/tapioca/generator.rb +231 -23
  35. data/lib/tapioca/loader.rb +30 -9
  36. data/lib/tapioca/version.rb +1 -1
  37. metadata +32 -39
@@ -0,0 +1,213 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+
6
+ begin
7
+ require "rails/railtie"
8
+ require "identity_cache"
9
+ rescue LoadError
10
+ # means IdentityCache is not installed,
11
+ # so let's not even define the generator.
12
+ return
13
+ end
14
+
15
+ module Tapioca
16
+ module Compilers
17
+ module Dsl
18
+ # `Tapioca::Compilers::DSL::ActiveRecordIdentityCache` generates RBI files for ActiveRecord models
19
+ # that use `include IdentityCache`
20
+ # `IdentityCache` is a blob level caching solution to plug into ActiveRecord. (see https://github.com/Shopify/identity_cache).
21
+ #
22
+ # For example, with the following ActiveRecord class:
23
+ #
24
+ # ~~~rb
25
+ # # post.rb
26
+ # class Post < ApplicationRecord
27
+ # include IdentityCache
28
+ #
29
+ # cache_index :blog_id
30
+ # cache_index :title, unique: true
31
+ # cache_index :title, :review_date, unique: true
32
+ #
33
+ # end
34
+ # ~~~
35
+ #
36
+ # this generator will produce the RBI file `post.rbi` with the following content:
37
+ #
38
+ # ~~~rbi
39
+ # # post.rbi
40
+ # # typed: true
41
+ # class Post
42
+ # sig { params(blog_id: T.untyped, includes: T.untyped).returns(T::Array[::Post])
43
+ # def fetch_by_blog_id(blog_id, includes: nil); end
44
+ #
45
+ # sig { params(blog_ids: T.untyped, includes: T.untyped).returns(T::Array[::Post])
46
+ # def fetch_multi_by_blog_id(index_values, includes: nil); end
47
+ #
48
+ # sig { params(title: T.untyped, includes: T.untyped).returns(::Post) }
49
+ # def fetch_by_title!(title, includes: nil); end
50
+ #
51
+ # sig { params(title: T.untyped, includes: T.untyped).returns(T.nilable(::Post)) }
52
+ # def fetch_by_title(title, includes: nil); end
53
+ #
54
+ # sig { params(index_values: T.untyped, includes: T.untyped).returns(T::Array[::Post]) }
55
+ # def fetch_multi_by_title(index_values, includes: nil); end
56
+ #
57
+ # sig { params(title: T.untyped, review_date: T.untyped, includes: T.untyped).returns(T::Array[::Post]) }
58
+ # def fetch_by_title_and_review_date!(title, review_date, includes: nil); end
59
+ #
60
+ # sig { params(title: T.untyped, review_date: T.untyped, includes: T.untyped).returns(T::Array[::Post]) }
61
+ # def fetch_by_title_and_review_date(title, review_date, includes: nil); end
62
+ # end
63
+ # ~~~
64
+
65
+ class ActiveRecordIdentityCache < Base
66
+ extend T::Sig
67
+
68
+ COLLECTION_TYPE = T.let(
69
+ ->(type) { "T::Array[::#{type}]" },
70
+ T.proc.params(type: Module).returns(String)
71
+ )
72
+
73
+ sig do
74
+ override
75
+ .params(
76
+ root: Parlour::RbiGenerator::Namespace,
77
+ constant: T.class_of(::ActiveRecord::Base)
78
+ )
79
+ .void
80
+ end
81
+ def decorate(root, constant)
82
+ caches = constant.send(:all_cached_associations)
83
+ cache_indexes = constant.send(:cache_indexes)
84
+ return if caches.empty? && cache_indexes.empty?
85
+
86
+ root.path(constant) do |k|
87
+ cache_manys = constant.send(:cached_has_manys)
88
+ cache_ones = constant.send(:cached_has_ones)
89
+ cache_belongs = constant.send(:cached_belongs_tos)
90
+
91
+ cache_indexes.each do |field|
92
+ create_fetch_by_methods(field, k, constant)
93
+ end
94
+
95
+ cache_manys.values.each do |field|
96
+ create_fetch_field_methods(field, k, returns_collection: true)
97
+ end
98
+
99
+ cache_ones.values.each do |field|
100
+ create_fetch_field_methods(field, k, returns_collection: false)
101
+ end
102
+
103
+ cache_belongs.values.each do |field|
104
+ create_fetch_field_methods(field, k, returns_collection: false)
105
+ end
106
+ end
107
+ end
108
+
109
+ sig { override.returns(T::Enumerable[Module]) }
110
+ def gather_constants
111
+ ::ActiveRecord::Base.descendants.select do |klass|
112
+ klass < IdentityCache
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ sig do
119
+ params(
120
+ field: T.untyped,
121
+ returns_collection: T::Boolean
122
+ )
123
+ .returns(String)
124
+ end
125
+ def type_for_field(field, returns_collection:)
126
+ cache_type = field.reflection.compute_class(field.reflection.class_name)
127
+ if returns_collection
128
+ COLLECTION_TYPE.call(cache_type)
129
+ else
130
+ "::#{cache_type}"
131
+ end
132
+ rescue ArgumentError
133
+ "T.untyped"
134
+ end
135
+
136
+ sig do
137
+ params(
138
+ field: T.untyped,
139
+ klass: Parlour::RbiGenerator::Namespace,
140
+ returns_collection: T::Boolean
141
+ )
142
+ .void
143
+ end
144
+ def create_fetch_field_methods(field, klass, returns_collection:)
145
+ name = field.cached_accessor_name.to_s
146
+ type = type_for_field(field, returns_collection: returns_collection)
147
+ klass.create_method(name, return_type: type)
148
+
149
+ if field.respond_to?(:cached_ids_name)
150
+ klass.create_method(field.cached_ids_name, return_type: "T::Array[T.untyped]")
151
+ elsif field.respond_to?(:cached_id_name)
152
+ klass.create_method(field.cached_id_name, return_type: "T.untyped")
153
+ end
154
+ end
155
+
156
+ sig do
157
+ params(
158
+ field: T.untyped,
159
+ klass: Parlour::RbiGenerator::Namespace,
160
+ constant: T.class_of(::ActiveRecord::Base),
161
+ )
162
+ .void
163
+ end
164
+ def create_fetch_by_methods(field, klass, constant)
165
+ field_length = field.key_fields.length
166
+ fields_name = field.key_fields.join("_and_")
167
+
168
+ parameters = field.key_fields.map do |arg|
169
+ Parlour::RbiGenerator::Parameter.new(arg.to_s, type: "T.untyped")
170
+ end
171
+ parameters << Parlour::RbiGenerator::Parameter.new("includes:", default: "nil", type: "T.untyped")
172
+
173
+ name = "fetch_by_#{fields_name}"
174
+ if field.unique
175
+ klass.create_method(
176
+ "#{name}!",
177
+ class_method: true,
178
+ parameters: parameters,
179
+ return_type: "::#{constant}"
180
+ )
181
+
182
+ klass.create_method(
183
+ name,
184
+ class_method: true,
185
+ parameters: parameters,
186
+ return_type: "T.nilable(::#{constant})"
187
+ )
188
+ else
189
+ klass.create_method(
190
+ name,
191
+ class_method: true,
192
+ parameters: parameters,
193
+ return_type: COLLECTION_TYPE.call(constant)
194
+ )
195
+ end
196
+
197
+ if field_length == 1
198
+ name = "fetch_multi_by_#{fields_name}"
199
+ klass.create_method(
200
+ name,
201
+ class_method: true,
202
+ parameters: [
203
+ Parlour::RbiGenerator::Parameter.new("index_values", type: "T.untyped"),
204
+ Parlour::RbiGenerator::Parameter.new("includes:", default: "nil", type: "T.untyped"),
205
+ ],
206
+ return_type: COLLECTION_TYPE.call(constant)
207
+ )
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,100 @@
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::ActiveRecordScope` decorates RBI files for subclasses of
16
+ # `ActiveRecord::Base` which declare `scope` fields
17
+ # (see https://api.rubyonrails.org).
18
+ #
19
+ # For example, with the following `ActiveRecord::Base` subclass:
20
+ #
21
+ # ~~~rb
22
+ # class Post < ApplicationRecord
23
+ # scope :public_kind, -> { where.not(kind: 'private') }
24
+ # scope :private_kind, -> { where(kind: 'private') }
25
+ # end
26
+ # ~~~
27
+ #
28
+ # this generator will produce the RBI file `post.rbi` with the following content:
29
+ #
30
+ # ~~~rbi
31
+ # # post.rbi
32
+ # # typed: true
33
+ # class Post
34
+ # extend Post::GeneratedRelationMethods
35
+ # end
36
+ #
37
+ # module Post::GeneratedRelationMethods
38
+ # sig { params(args: T.untyped, blk: T.untyped).returns(T.untyped) }
39
+ # def private_kind(*args, &blk); end
40
+
41
+ # sig { params(args: T.untyped, blk: T.untyped).returns(T.untyped) }
42
+ # def public_kind(*args, &blk); end
43
+ # end
44
+ # ~~~
45
+ class ActiveRecordScope < Base
46
+ extend T::Sig
47
+
48
+ sig do
49
+ override.params(
50
+ root: Parlour::RbiGenerator::Namespace,
51
+ constant: T.class_of(::ActiveRecord::Base)
52
+ ).void
53
+ end
54
+ def decorate(root, constant)
55
+ scope_method_names = constant.send(:generated_relation_methods).instance_methods(false)
56
+ return if scope_method_names.empty?
57
+
58
+ module_name = "#{constant}::GeneratedRelationMethods"
59
+ root.create_module(module_name) do |mod|
60
+ scope_method_names.each do |scope_method|
61
+ generate_scope_method(scope_method, mod)
62
+ end
63
+ end
64
+
65
+ root.path(constant) do |k|
66
+ k.create_extend(module_name)
67
+ end
68
+ end
69
+
70
+ sig { override.returns(T::Enumerable[Module]) }
71
+ def gather_constants
72
+ ::ActiveRecord::Base.descendants.reject(&:abstract_class?)
73
+ end
74
+
75
+ private
76
+
77
+ sig do
78
+ params(
79
+ scope_method: String,
80
+ mod: Parlour::RbiGenerator::Namespace,
81
+ ).void
82
+ end
83
+ def generate_scope_method(scope_method, mod)
84
+ # This return type should actually be Model::ActiveRecord_Relation
85
+ return_type = "T.untyped"
86
+
87
+ create_method(
88
+ mod,
89
+ scope_method,
90
+ parameters: [
91
+ Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped"),
92
+ Parlour::RbiGenerator::Parameter.new("&blk", type: "T.untyped"),
93
+ ],
94
+ return_type: return_type,
95
+ )
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,170 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+ require "tapioca/core_ext/class"
6
+
7
+ begin
8
+ require "activerecord-typedstore"
9
+ rescue LoadError
10
+ # means ActiveRecord::TypedStore is not installed,
11
+ # so let's not even define the generator.
12
+ return
13
+ end
14
+
15
+ module Tapioca
16
+ module Compilers
17
+ module Dsl
18
+ # `Tapioca::Compilers::DSL::ActiveRecordTypedStore` generates RBI files for ActiveRecord models that use
19
+ # `ActiveRecord::TypedStore` features (see https://github.com/byroot/activerecord-typedstore).
20
+ #
21
+ # For example, with the following ActiveRecord class:
22
+ #
23
+ # ~~~rb
24
+ # # post.rb
25
+ # class Post < ApplicationRecord
26
+ # typed_store :metadata do |s|
27
+ # s.string(:reviewer, blank: false, accessor: false)
28
+ # s.date(:review_date)
29
+ # s.boolean(:reviewed, null: false, default: false)
30
+ # end
31
+ # end
32
+ # ~~~
33
+ #
34
+ # this generator will produce the RBI file `post.rbi` with the following content:
35
+ #
36
+ # ~~~rbi
37
+ # # post.rbi
38
+ # # typed: true
39
+ # class Post
40
+ # sig { params(review_date: T.nilable(Date)).returns(T.nilable(Date)) }
41
+ # def review_date=(review_date); end
42
+ #
43
+ # sig { returns(T.nilable(Date)) }
44
+ # def review_date; end
45
+ #
46
+ # sig { returns(T.nilable(Date)) }
47
+ # def review_date_was; end
48
+ #
49
+ # sig { returns(T::Boolean) }
50
+ # def review_date_changed?; end
51
+ #
52
+ # sig { returns(T.nilable(Date)) }
53
+ # def review_date_before_last_save; end
54
+ #
55
+ # sig { returns(T::Boolean) }
56
+ # def saved_change_to_review_date?; end
57
+ #
58
+ # sig { returns(T.nilable([T.nilable(Date), T.nilable(Date)])) }
59
+ # def review_date_change; end
60
+ #
61
+ # sig { returns(T.nilable([T.nilable(Date), T.nilable(Date)])) }
62
+ # def saved_change_to_review_date; end
63
+ #
64
+ # sig { params(reviewd: T::Boolean).returns(T::Boolean) }
65
+ # def reviewed=(reviewed); end
66
+ #
67
+ # sig { returns(T::Boolean) }
68
+ # def reviewed; end
69
+ #
70
+ # sig { returns(T::Boolean) }
71
+ # def reviewed_was; end
72
+ #
73
+ # sig { returns(T::Boolean) }
74
+ # def reviewed_changed?; end
75
+ #
76
+ # sig { returns(T::Boolean) }
77
+ # def reviewed_before_last_save; end
78
+ #
79
+ # sig { returns(T::Boolean) }
80
+ # def saved_change_to_reviewed?; end
81
+ #
82
+ # sig { returns(T.nilable([T::Boolean, T::Boolean])) }
83
+ # def reviewed_change; end
84
+ #
85
+ # sig { returns(T.nilable([T::Boolean, T::Boolean])) }
86
+ # def saved_change_to_reviewed; end
87
+ # end
88
+ # ~~~
89
+ # end
90
+
91
+ class ActiveRecordTypedStore < Base
92
+ extend T::Sig
93
+
94
+ sig do
95
+ override
96
+ .params(
97
+ root: Parlour::RbiGenerator::Namespace,
98
+ constant: T.class_of(::ActiveRecord::Base)
99
+ )
100
+ .void
101
+ end
102
+ def decorate(root, constant)
103
+ stores = constant.typed_stores
104
+ return if stores.values.flat_map(&:accessors).empty?
105
+
106
+ root.path(constant) do |k|
107
+ stores.values.each do |store_data|
108
+ store_data.accessors.each do |accessor|
109
+ field = store_data.fields[accessor]
110
+ type = type_for(field.type_sym)
111
+ type = "T.nilable(#{type})" if field.null && type != "T.untyped"
112
+ generate_methods(field.name.to_s, type, k)
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ sig { override.returns(T::Enumerable[Module]) }
119
+ def gather_constants
120
+ ::ActiveRecord::Base.descendants.select do |klass|
121
+ klass.include?(ActiveRecord::TypedStore::Behavior)
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ TYPES = T.let({
128
+ boolean: "T::Boolean",
129
+ integer: "Integer",
130
+ string: "String",
131
+ float: "Float",
132
+ date: "Date",
133
+ time: "Time",
134
+ datetime: "DateTime",
135
+ decimal: "BigDecimal",
136
+ any: "T.untyped",
137
+ }.freeze, T::Hash[Symbol, String])
138
+
139
+ sig { params(attr_type: Symbol).returns(String) }
140
+ def type_for(attr_type)
141
+ TYPES.fetch(attr_type, "T.untyped")
142
+ end
143
+
144
+ sig do
145
+ params(
146
+ name: String,
147
+ type: String,
148
+ klass: Parlour::RbiGenerator::Namespace
149
+ )
150
+ .void
151
+ end
152
+ def generate_methods(name, type, klass)
153
+ klass.create_method(
154
+ "#{name}=",
155
+ parameters: [Parlour::RbiGenerator::Parameter.new(name, type: type)],
156
+ return_type: type
157
+ )
158
+ klass.create_method(name, return_type: type)
159
+ klass.create_method("#{name}?", return_type: "T::Boolean")
160
+ klass.create_method("#{name}_was", return_type: type)
161
+ klass.create_method("#{name}_changed?", return_type: "T::Boolean")
162
+ klass.create_method("#{name}_before_last_save", return_type: type)
163
+ klass.create_method("saved_change_to_#{name}?", return_type: "T::Boolean")
164
+ klass.create_method("#{name}_change", return_type: "T.nilable([#{type}, #{type}])")
165
+ klass.create_method("saved_change_to_#{name}", return_type: "T.nilable([#{type}, #{type}])")
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end