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,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