tapioca 0.2.8 → 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +25 -1
- data/README.md +23 -2
- data/Rakefile +15 -4
- data/lib/tapioca.rb +15 -9
- data/lib/tapioca/cli.rb +41 -12
- 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 +285 -0
- data/lib/tapioca/compilers/dsl/active_record_columns.rb +387 -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 +83 -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 +210 -50
- data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +3 -17
- data/lib/tapioca/compilers/todos_compiler.rb +32 -0
- data/lib/tapioca/config.rb +42 -0
- data/lib/tapioca/config_builder.rb +75 -0
- data/lib/tapioca/constant_locator.rb +1 -0
- data/lib/tapioca/core_ext/class.rb +23 -0
- data/lib/tapioca/gemfile.rb +44 -9
- data/lib/tapioca/generator.rb +248 -70
- data/lib/tapioca/loader.rb +20 -9
- data/lib/tapioca/sorbet_config_parser.rb +77 -0
- data/lib/tapioca/version.rb +1 -1
- metadata +33 -51
@@ -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
|
@@ -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
|