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.
- checksums.yaml +4 -4
- data/Gemfile +25 -1
- data/README.md +23 -2
- data/Rakefile +15 -4
- data/lib/tapioca.rb +8 -2
- data/lib/tapioca/cli.rb +32 -3
- data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +129 -0
- data/lib/tapioca/compilers/dsl/action_mailer.rb +65 -0
- data/lib/tapioca/compilers/dsl/active_record_associations.rb +267 -0
- data/lib/tapioca/compilers/dsl/active_record_columns.rb +393 -0
- data/lib/tapioca/compilers/dsl/active_record_enum.rb +112 -0
- data/lib/tapioca/compilers/dsl/active_record_identity_cache.rb +213 -0
- data/lib/tapioca/compilers/dsl/active_record_scope.rb +100 -0
- data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +170 -0
- data/lib/tapioca/compilers/dsl/active_resource.rb +140 -0
- data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +126 -0
- data/lib/tapioca/compilers/dsl/base.rb +165 -0
- data/lib/tapioca/compilers/dsl/frozen_record.rb +96 -0
- data/lib/tapioca/compilers/dsl/protobuf.rb +144 -0
- data/lib/tapioca/compilers/dsl/smart_properties.rb +173 -0
- data/lib/tapioca/compilers/dsl/state_machines.rb +378 -0
- data/lib/tapioca/compilers/dsl/url_helpers.rb +92 -0
- data/lib/tapioca/compilers/dsl_compiler.rb +121 -0
- data/lib/tapioca/compilers/requires_compiler.rb +67 -0
- data/lib/tapioca/compilers/sorbet.rb +34 -0
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +171 -26
- data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +1 -20
- data/lib/tapioca/compilers/todos_compiler.rb +32 -0
- data/lib/tapioca/config.rb +14 -6
- data/lib/tapioca/config_builder.rb +22 -9
- data/lib/tapioca/constant_locator.rb +1 -0
- data/lib/tapioca/core_ext/class.rb +23 -0
- data/lib/tapioca/gemfile.rb +32 -9
- data/lib/tapioca/generator.rb +231 -23
- data/lib/tapioca/loader.rb +30 -9
- data/lib/tapioca/version.rb +1 -1
- 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
|