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,212 @@
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
+ class ActiveRecordIdentityCache < Base
65
+ extend T::Sig
66
+
67
+ COLLECTION_TYPE = T.let(
68
+ ->(type) { "T::Array[::#{type}]" },
69
+ T.proc.params(type: Module).returns(String)
70
+ )
71
+
72
+ sig do
73
+ override
74
+ .params(
75
+ root: Parlour::RbiGenerator::Namespace,
76
+ constant: T.class_of(::ActiveRecord::Base)
77
+ )
78
+ .void
79
+ end
80
+ def decorate(root, constant)
81
+ caches = constant.send(:all_cached_associations)
82
+ cache_indexes = constant.send(:cache_indexes)
83
+ return if caches.empty? && cache_indexes.empty?
84
+
85
+ root.path(constant) do |k|
86
+ cache_manys = constant.send(:cached_has_manys)
87
+ cache_ones = constant.send(:cached_has_ones)
88
+ cache_belongs = constant.send(:cached_belongs_tos)
89
+
90
+ cache_indexes.each do |field|
91
+ create_fetch_by_methods(field, k, constant)
92
+ end
93
+
94
+ cache_manys.values.each do |field|
95
+ create_fetch_field_methods(field, k, returns_collection: true)
96
+ end
97
+
98
+ cache_ones.values.each do |field|
99
+ create_fetch_field_methods(field, k, returns_collection: false)
100
+ end
101
+
102
+ cache_belongs.values.each do |field|
103
+ create_fetch_field_methods(field, k, returns_collection: false)
104
+ end
105
+ end
106
+ end
107
+
108
+ sig { override.returns(T::Enumerable[Module]) }
109
+ def gather_constants
110
+ ::ActiveRecord::Base.descendants.select do |klass|
111
+ klass < IdentityCache
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ sig do
118
+ params(
119
+ field: T.untyped,
120
+ returns_collection: T::Boolean
121
+ )
122
+ .returns(String)
123
+ end
124
+ def type_for_field(field, returns_collection:)
125
+ cache_type = field.reflection.compute_class(field.reflection.class_name)
126
+ if returns_collection
127
+ COLLECTION_TYPE.call(cache_type)
128
+ else
129
+ "::#{cache_type}"
130
+ end
131
+ rescue ArgumentError
132
+ "T.untyped"
133
+ end
134
+
135
+ sig do
136
+ params(
137
+ field: T.untyped,
138
+ klass: Parlour::RbiGenerator::Namespace,
139
+ returns_collection: T::Boolean
140
+ )
141
+ .void
142
+ end
143
+ def create_fetch_field_methods(field, klass, returns_collection:)
144
+ name = field.cached_accessor_name.to_s
145
+ type = type_for_field(field, returns_collection: returns_collection)
146
+ klass.create_method(name, return_type: type)
147
+
148
+ if field.respond_to?(:cached_ids_name)
149
+ klass.create_method(field.cached_ids_name, return_type: "T::Array[T.untyped]")
150
+ elsif field.respond_to?(:cached_id_name)
151
+ klass.create_method(field.cached_id_name, return_type: "T.untyped")
152
+ end
153
+ end
154
+
155
+ sig do
156
+ params(
157
+ field: T.untyped,
158
+ klass: Parlour::RbiGenerator::Namespace,
159
+ constant: T.class_of(::ActiveRecord::Base),
160
+ )
161
+ .void
162
+ end
163
+ def create_fetch_by_methods(field, klass, constant)
164
+ field_length = field.key_fields.length
165
+ fields_name = field.key_fields.join("_and_")
166
+
167
+ parameters = field.key_fields.map do |arg|
168
+ Parlour::RbiGenerator::Parameter.new(arg.to_s, type: "T.untyped")
169
+ end
170
+ parameters << Parlour::RbiGenerator::Parameter.new("includes:", default: "nil", type: "T.untyped")
171
+
172
+ name = "fetch_by_#{fields_name}"
173
+ if field.unique
174
+ klass.create_method(
175
+ "#{name}!",
176
+ class_method: true,
177
+ parameters: parameters,
178
+ return_type: "::#{constant}"
179
+ )
180
+
181
+ klass.create_method(
182
+ name,
183
+ class_method: true,
184
+ parameters: parameters,
185
+ return_type: "T.nilable(::#{constant})"
186
+ )
187
+ else
188
+ klass.create_method(
189
+ name,
190
+ class_method: true,
191
+ parameters: parameters,
192
+ return_type: COLLECTION_TYPE.call(constant)
193
+ )
194
+ end
195
+
196
+ if field_length == 1
197
+ name = "fetch_multi_by_#{fields_name}"
198
+ klass.create_method(
199
+ name,
200
+ class_method: true,
201
+ parameters: [
202
+ Parlour::RbiGenerator::Parameter.new("index_values", type: "T.untyped"),
203
+ Parlour::RbiGenerator::Parameter.new("includes:", default: "nil", type: "T.untyped"),
204
+ ],
205
+ return_type: COLLECTION_TYPE.call(constant)
206
+ )
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
212
+ 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,168 @@
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
+ class ActiveRecordTypedStore < Base
90
+ extend T::Sig
91
+
92
+ sig do
93
+ override
94
+ .params(
95
+ root: Parlour::RbiGenerator::Namespace,
96
+ constant: T.class_of(::ActiveRecord::Base)
97
+ )
98
+ .void
99
+ end
100
+ def decorate(root, constant)
101
+ stores = constant.typed_stores
102
+ return if stores.values.flat_map(&:accessors).empty?
103
+
104
+ root.path(constant) do |k|
105
+ stores.values.each do |store_data|
106
+ store_data.accessors.each do |accessor|
107
+ field = store_data.fields[accessor]
108
+ type = type_for(field.type_sym)
109
+ type = "T.nilable(#{type})" if field.null && type != "T.untyped"
110
+ generate_methods(field.name.to_s, type, k)
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ sig { override.returns(T::Enumerable[Module]) }
117
+ def gather_constants
118
+ ::ActiveRecord::Base.descendants.select do |klass|
119
+ klass.include?(ActiveRecord::TypedStore::Behavior)
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ TYPES = T.let({
126
+ boolean: "T::Boolean",
127
+ integer: "Integer",
128
+ string: "String",
129
+ float: "Float",
130
+ date: "Date",
131
+ time: "Time",
132
+ datetime: "DateTime",
133
+ decimal: "BigDecimal",
134
+ any: "T.untyped",
135
+ }.freeze, T::Hash[Symbol, String])
136
+
137
+ sig { params(attr_type: Symbol).returns(String) }
138
+ def type_for(attr_type)
139
+ TYPES.fetch(attr_type, "T.untyped")
140
+ end
141
+
142
+ sig do
143
+ params(
144
+ name: String,
145
+ type: String,
146
+ klass: Parlour::RbiGenerator::Namespace
147
+ )
148
+ .void
149
+ end
150
+ def generate_methods(name, type, klass)
151
+ klass.create_method(
152
+ "#{name}=",
153
+ parameters: [Parlour::RbiGenerator::Parameter.new(name, type: type)],
154
+ return_type: type
155
+ )
156
+ klass.create_method(name, return_type: type)
157
+ klass.create_method("#{name}?", return_type: "T::Boolean")
158
+ klass.create_method("#{name}_was", return_type: type)
159
+ klass.create_method("#{name}_changed?", return_type: "T::Boolean")
160
+ klass.create_method("#{name}_before_last_save", return_type: type)
161
+ klass.create_method("saved_change_to_#{name}?", return_type: "T::Boolean")
162
+ klass.create_method("#{name}_change", return_type: "T.nilable([#{type}, #{type}])")
163
+ klass.create_method("saved_change_to_#{name}", return_type: "T.nilable([#{type}, #{type}])")
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end