tapioca 0.4.23 → 0.5.0
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 +14 -14
- data/README.md +2 -2
- data/Rakefile +5 -7
- data/exe/tapioca +2 -2
- data/lib/tapioca/cli.rb +256 -2
- data/lib/tapioca/compilers/dsl/aasm.rb +122 -0
- data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +52 -12
- data/lib/tapioca/compilers/dsl/action_mailer.rb +6 -9
- data/lib/tapioca/compilers/dsl/active_job.rb +8 -12
- data/lib/tapioca/compilers/dsl/active_model_attributes.rb +131 -0
- data/lib/tapioca/compilers/dsl/active_record_associations.rb +33 -54
- data/lib/tapioca/compilers/dsl/active_record_columns.rb +10 -105
- data/lib/tapioca/compilers/dsl/active_record_enum.rb +8 -10
- data/lib/tapioca/compilers/dsl/active_record_scope.rb +7 -10
- data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +5 -8
- data/lib/tapioca/compilers/dsl/active_resource.rb +9 -37
- data/lib/tapioca/compilers/dsl/active_storage.rb +98 -0
- data/lib/tapioca/compilers/dsl/active_support_concern.rb +108 -0
- data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +13 -8
- data/lib/tapioca/compilers/dsl/base.rb +96 -82
- data/lib/tapioca/compilers/dsl/config.rb +111 -0
- data/lib/tapioca/compilers/dsl/frozen_record.rb +5 -7
- data/lib/tapioca/compilers/dsl/identity_cache.rb +66 -29
- data/lib/tapioca/compilers/dsl/protobuf.rb +19 -69
- data/lib/tapioca/compilers/dsl/sidekiq_worker.rb +25 -12
- data/lib/tapioca/compilers/dsl/smart_properties.rb +19 -31
- data/lib/tapioca/compilers/dsl/state_machines.rb +56 -78
- data/lib/tapioca/compilers/dsl/url_helpers.rb +7 -10
- data/lib/tapioca/compilers/dsl_compiler.rb +22 -38
- data/lib/tapioca/compilers/requires_compiler.rb +2 -2
- data/lib/tapioca/compilers/sorbet.rb +26 -5
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +139 -154
- data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +4 -4
- data/lib/tapioca/compilers/symbol_table_compiler.rb +1 -1
- data/lib/tapioca/compilers/todos_compiler.rb +1 -1
- data/lib/tapioca/config.rb +2 -0
- data/lib/tapioca/config_builder.rb +4 -2
- data/lib/tapioca/constant_locator.rb +6 -8
- data/lib/tapioca/gemfile.rb +26 -19
- data/lib/tapioca/generator.rb +127 -43
- data/lib/tapioca/generic_type_registry.rb +25 -98
- data/lib/tapioca/helpers/active_record_column_type_helper.rb +98 -0
- data/lib/tapioca/internal.rb +1 -9
- data/lib/tapioca/loader.rb +14 -48
- data/lib/tapioca/rbi_ext/model.rb +122 -0
- data/lib/tapioca/reflection.rb +131 -0
- data/lib/tapioca/sorbet_ext/fixed_hash_patch.rb +1 -1
- data/lib/tapioca/sorbet_ext/generic_name_patch.rb +72 -4
- data/lib/tapioca/sorbet_ext/name_patch.rb +1 -1
- data/lib/tapioca/version.rb +1 -1
- data/lib/tapioca.rb +2 -0
- metadata +35 -23
- data/lib/tapioca/cli/main.rb +0 -146
- data/lib/tapioca/core_ext/class.rb +0 -28
- data/lib/tapioca/core_ext/string.rb +0 -18
- data/lib/tapioca/rbi/model.rb +0 -405
- data/lib/tapioca/rbi/printer.rb +0 -410
- data/lib/tapioca/rbi/rewriters/group_nodes.rb +0 -106
- data/lib/tapioca/rbi/rewriters/nest_non_public_methods.rb +0 -65
- data/lib/tapioca/rbi/rewriters/nest_singleton_methods.rb +0 -42
- data/lib/tapioca/rbi/rewriters/sort_nodes.rb +0 -82
- data/lib/tapioca/rbi/visitor.rb +0 -21
@@ -1,8 +1,6 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require "parlour"
|
5
|
-
|
6
4
|
begin
|
7
5
|
require "frozen_record"
|
8
6
|
rescue LoadError
|
@@ -67,18 +65,18 @@ module Tapioca
|
|
67
65
|
class FrozenRecord < Base
|
68
66
|
extend T::Sig
|
69
67
|
|
70
|
-
sig { override.params(root:
|
68
|
+
sig { override.params(root: RBI::Tree, constant: T.class_of(::FrozenRecord::Base)).void }
|
71
69
|
def decorate(root, constant)
|
72
70
|
attributes = constant.attributes
|
73
71
|
return if attributes.empty?
|
74
72
|
|
75
|
-
root.
|
73
|
+
root.create_path(constant) do |record|
|
76
74
|
module_name = "FrozenRecordAttributeMethods"
|
77
75
|
|
78
76
|
record.create_module(module_name) do |mod|
|
79
77
|
attributes.each do |attribute|
|
80
|
-
create_method(
|
81
|
-
create_method(
|
78
|
+
mod.create_method("#{attribute}?", return_type: "T::Boolean")
|
79
|
+
mod.create_method(attribute.to_s, return_type: "T.untyped")
|
82
80
|
end
|
83
81
|
end
|
84
82
|
|
@@ -88,7 +86,7 @@ module Tapioca
|
|
88
86
|
|
89
87
|
sig { override.returns(T::Enumerable[Module]) }
|
90
88
|
def gather_constants
|
91
|
-
::FrozenRecord::Base.
|
89
|
+
descendants_of(::FrozenRecord::Base).reject(&:abstract_class?)
|
92
90
|
end
|
93
91
|
end
|
94
92
|
end
|
@@ -1,8 +1,6 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require "parlour"
|
5
|
-
|
6
4
|
begin
|
7
5
|
require "rails/railtie"
|
8
6
|
require "identity_cache"
|
@@ -67,23 +65,16 @@ module Tapioca
|
|
67
65
|
|
68
66
|
COLLECTION_TYPE = T.let(
|
69
67
|
->(type) { "T::Array[::#{type}]" },
|
70
|
-
T.proc.params(type: Module).returns(String)
|
68
|
+
T.proc.params(type: T.any(Module, String)).returns(String)
|
71
69
|
)
|
72
70
|
|
73
|
-
sig
|
74
|
-
override
|
75
|
-
.params(
|
76
|
-
root: Parlour::RbiGenerator::Namespace,
|
77
|
-
constant: T.class_of(::ActiveRecord::Base)
|
78
|
-
)
|
79
|
-
.void
|
80
|
-
end
|
71
|
+
sig { override.params(root: RBI::Tree, constant: T.class_of(::ActiveRecord::Base)).void }
|
81
72
|
def decorate(root, constant)
|
82
73
|
caches = constant.send(:all_cached_associations)
|
83
74
|
cache_indexes = constant.send(:cache_indexes)
|
84
75
|
return if caches.empty? && cache_indexes.empty?
|
85
76
|
|
86
|
-
root.
|
77
|
+
root.create_path(constant) do |model|
|
87
78
|
cache_manys = constant.send(:cached_has_manys)
|
88
79
|
cache_ones = constant.send(:cached_has_ones)
|
89
80
|
cache_belongs = constant.send(:cached_belongs_tos)
|
@@ -108,7 +99,7 @@ module Tapioca
|
|
108
99
|
|
109
100
|
sig { override.returns(T::Enumerable[Module]) }
|
110
101
|
def gather_constants
|
111
|
-
::ActiveRecord::Base.
|
102
|
+
descendants_of(::ActiveRecord::Base).select do |klass|
|
112
103
|
klass < ::IdentityCache::WithoutPrimaryIndex
|
113
104
|
end
|
114
105
|
end
|
@@ -119,8 +110,7 @@ module Tapioca
|
|
119
110
|
params(
|
120
111
|
field: T.untyped,
|
121
112
|
returns_collection: T::Boolean
|
122
|
-
)
|
123
|
-
.returns(String)
|
113
|
+
).returns(String)
|
124
114
|
end
|
125
115
|
def type_for_field(field, returns_collection:)
|
126
116
|
cache_type = field.reflection.compute_class(field.reflection.class_name)
|
@@ -136,10 +126,9 @@ module Tapioca
|
|
136
126
|
sig do
|
137
127
|
params(
|
138
128
|
field: T.untyped,
|
139
|
-
klass:
|
129
|
+
klass: RBI::Scope,
|
140
130
|
returns_collection: T::Boolean
|
141
|
-
)
|
142
|
-
.void
|
131
|
+
).void
|
143
132
|
end
|
144
133
|
def create_fetch_field_methods(field, klass, returns_collection:)
|
145
134
|
name = field.cached_accessor_name.to_s
|
@@ -156,21 +145,36 @@ module Tapioca
|
|
156
145
|
sig do
|
157
146
|
params(
|
158
147
|
field: T.untyped,
|
159
|
-
klass:
|
148
|
+
klass: RBI::Scope,
|
160
149
|
constant: T.class_of(::ActiveRecord::Base),
|
161
|
-
)
|
162
|
-
.void
|
150
|
+
).void
|
163
151
|
end
|
164
152
|
def create_fetch_by_methods(field, klass, constant)
|
153
|
+
is_cache_index = field.instance_variable_defined?(:@attribute_proc)
|
154
|
+
|
155
|
+
# Both `cache_index` and `cache_attribute` generate aliased methods
|
156
|
+
create_aliased_fetch_by_methods(field, klass, constant)
|
157
|
+
|
158
|
+
# If the method used was `cache_index` a few extra methods are created
|
159
|
+
create_index_fetch_by_methods(field, klass, constant) if is_cache_index
|
160
|
+
end
|
161
|
+
|
162
|
+
sig do
|
163
|
+
params(
|
164
|
+
field: T.untyped,
|
165
|
+
klass: RBI::Scope,
|
166
|
+
constant: T.class_of(::ActiveRecord::Base),
|
167
|
+
).void
|
168
|
+
end
|
169
|
+
def create_index_fetch_by_methods(field, klass, constant)
|
165
170
|
field_length = field.key_fields.length
|
166
171
|
fields_name = field.key_fields.join("_and_")
|
167
|
-
|
172
|
+
name = "fetch_by_#{fields_name}"
|
168
173
|
parameters = field.key_fields.map do |arg|
|
169
|
-
|
174
|
+
create_param(arg.to_s, type: "T.untyped")
|
170
175
|
end
|
171
|
-
parameters <<
|
176
|
+
parameters << create_kw_opt_param("includes", default: "nil", type: "T.untyped")
|
172
177
|
|
173
|
-
name = "fetch_by_#{fields_name}"
|
174
178
|
if field.unique
|
175
179
|
klass.create_method(
|
176
180
|
"#{name}!",
|
@@ -195,18 +199,51 @@ module Tapioca
|
|
195
199
|
end
|
196
200
|
|
197
201
|
if field_length == 1
|
198
|
-
name = "fetch_multi_by_#{fields_name}"
|
199
202
|
klass.create_method(
|
200
|
-
|
203
|
+
"fetch_multi_by_#{fields_name}",
|
201
204
|
class_method: true,
|
202
205
|
parameters: [
|
203
|
-
|
204
|
-
|
206
|
+
create_param("index_values", type: "T::Enumerable[T.untyped]"),
|
207
|
+
create_kw_opt_param("includes", default: "nil", type: "T.untyped"),
|
205
208
|
],
|
206
209
|
return_type: COLLECTION_TYPE.call(constant)
|
207
210
|
)
|
208
211
|
end
|
209
212
|
end
|
213
|
+
|
214
|
+
sig do
|
215
|
+
params(
|
216
|
+
field: T.untyped,
|
217
|
+
klass: RBI::Scope,
|
218
|
+
constant: T.class_of(::ActiveRecord::Base),
|
219
|
+
).void
|
220
|
+
end
|
221
|
+
def create_aliased_fetch_by_methods(field, klass, constant)
|
222
|
+
type, _ = ActiveRecordColumnTypeHelper.new(constant).type_for(field.alias_name.to_s)
|
223
|
+
multi_type = type.delete_prefix("T.nilable(").delete_suffix(")").delete_prefix("::")
|
224
|
+
length = field.key_fields.length
|
225
|
+
suffix = field.send(:fetch_method_suffix)
|
226
|
+
|
227
|
+
parameters = field.key_fields.map do |arg|
|
228
|
+
create_param(arg.to_s, type: "T.untyped")
|
229
|
+
end
|
230
|
+
|
231
|
+
klass.create_method(
|
232
|
+
"fetch_#{suffix}",
|
233
|
+
class_method: true,
|
234
|
+
parameters: parameters,
|
235
|
+
return_type: type
|
236
|
+
)
|
237
|
+
|
238
|
+
if length == 1
|
239
|
+
klass.create_method(
|
240
|
+
"fetch_multi_#{suffix}",
|
241
|
+
class_method: true,
|
242
|
+
parameters: [create_param("keys", type: "T::Enumerable[T.untyped]")],
|
243
|
+
return_type: COLLECTION_TYPE.call(multi_type)
|
244
|
+
)
|
245
|
+
end
|
246
|
+
end
|
210
247
|
end
|
211
248
|
end
|
212
249
|
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
|
-
require "parlour"
|
4
3
|
|
5
4
|
begin
|
6
5
|
require "google/protobuf"
|
@@ -62,67 +61,18 @@ module Tapioca
|
|
62
61
|
# end
|
63
62
|
# ~~~
|
64
63
|
class Protobuf < Base
|
65
|
-
# Parlour doesn't support type members out of the box, so adding the
|
66
|
-
# ability to do that here. This should be upstreamed.
|
67
|
-
class TypeMember < Parlour::RbiGenerator::RbiObject
|
68
|
-
extend T::Sig
|
69
|
-
|
70
|
-
sig { params(other: Object).returns(T::Boolean) }
|
71
|
-
def ==(other)
|
72
|
-
TypeMember === other && name == other.name
|
73
|
-
end
|
74
|
-
|
75
|
-
sig do
|
76
|
-
override
|
77
|
-
.params(indent_level: Integer, options: Parlour::RbiGenerator::Options)
|
78
|
-
.returns(T::Array[String])
|
79
|
-
end
|
80
|
-
def generate_rbi(indent_level, options)
|
81
|
-
[options.indented(indent_level, "#{name} = type_member")]
|
82
|
-
end
|
83
|
-
|
84
|
-
sig do
|
85
|
-
override
|
86
|
-
.params(others: T::Array[Parlour::RbiGenerator::RbiObject])
|
87
|
-
.returns(T::Boolean)
|
88
|
-
end
|
89
|
-
def mergeable?(others)
|
90
|
-
others.all? { |other| self == other }
|
91
|
-
end
|
92
|
-
|
93
|
-
sig { override.params(others: T::Array[Parlour::RbiGenerator::RbiObject]).void }
|
94
|
-
def merge_into_self(others); end
|
95
|
-
|
96
|
-
sig { override.returns(String) }
|
97
|
-
def describe
|
98
|
-
"Type Member (#{name})"
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
64
|
class Field < T::Struct
|
103
65
|
prop :name, String
|
104
66
|
prop :type, String
|
105
67
|
prop :init_type, String
|
106
68
|
prop :default, String
|
107
|
-
|
108
|
-
extend T::Sig
|
109
|
-
|
110
|
-
sig { returns(Parlour::RbiGenerator::Parameter) }
|
111
|
-
def to_init
|
112
|
-
Parlour::RbiGenerator::Parameter.new("#{name}:", type: init_type, default: default)
|
113
|
-
end
|
114
69
|
end
|
115
70
|
|
116
71
|
extend T::Sig
|
117
72
|
|
118
|
-
sig
|
119
|
-
override.params(
|
120
|
-
root: Parlour::RbiGenerator::Namespace,
|
121
|
-
constant: Module
|
122
|
-
).void
|
123
|
-
end
|
73
|
+
sig { override.params(root: RBI::Tree, constant: Module).void }
|
124
74
|
def decorate(root, constant)
|
125
|
-
root.
|
75
|
+
root.create_path(constant) do |klass|
|
126
76
|
if constant == Google::Protobuf::RepeatedField
|
127
77
|
create_type_members(klass, "Elem")
|
128
78
|
elsif constant == Google::Protobuf::Map
|
@@ -132,7 +82,11 @@ module Tapioca
|
|
132
82
|
fields = descriptor.map { |desc| create_descriptor_method(klass, desc) }
|
133
83
|
fields.sort_by!(&:name)
|
134
84
|
|
135
|
-
|
85
|
+
parameters = fields.map do |field|
|
86
|
+
create_kw_opt_param(field.name, type: field.init_type, default: field.default)
|
87
|
+
end
|
88
|
+
|
89
|
+
klass.create_method("initialize", parameters: parameters, return_type: "void")
|
136
90
|
end
|
137
91
|
end
|
138
92
|
end
|
@@ -146,12 +100,12 @@ module Tapioca
|
|
146
100
|
|
147
101
|
private
|
148
102
|
|
149
|
-
sig { params(klass:
|
103
|
+
sig { params(klass: RBI::Scope, names: String).void }
|
150
104
|
def create_type_members(klass, *names)
|
151
105
|
klass.create_extend("T::Generic")
|
152
106
|
|
153
107
|
names.each do |name|
|
154
|
-
klass.
|
108
|
+
klass.create_type_member(name)
|
155
109
|
end
|
156
110
|
end
|
157
111
|
|
@@ -186,34 +140,34 @@ module Tapioca
|
|
186
140
|
# how Google names map entries.
|
187
141
|
# https://github.com/protocolbuffers/protobuf/blob/f82e26/ruby/ext/google/protobuf_c/defs.c#L1963-L1966
|
188
142
|
if descriptor.submsg_name.to_s.end_with?("_MapEntry_#{descriptor.name}")
|
189
|
-
key = descriptor.subtype.lookup(
|
190
|
-
value = descriptor.subtype.lookup(
|
143
|
+
key = descriptor.subtype.lookup("key")
|
144
|
+
value = descriptor.subtype.lookup("value")
|
191
145
|
|
192
146
|
key_type = type_of(key)
|
193
147
|
value_type = type_of(value)
|
194
148
|
type = "Google::Protobuf::Map[#{key_type}, #{value_type}]"
|
195
149
|
|
196
150
|
default_args = [key.type.inspect, value.type.inspect]
|
197
|
-
default_args << value_type if
|
151
|
+
default_args << value_type if [:enum, :message].include?(value.type)
|
198
152
|
|
199
153
|
Field.new(
|
200
154
|
name: descriptor.name,
|
201
155
|
type: type,
|
202
156
|
init_type: "T.any(#{type}, T::Hash[#{key_type}, #{value_type}])",
|
203
|
-
default: "Google::Protobuf::Map.new(#{default_args.join(
|
157
|
+
default: "Google::Protobuf::Map.new(#{default_args.join(", ")})"
|
204
158
|
)
|
205
159
|
else
|
206
160
|
elem_type = type_of(descriptor)
|
207
161
|
type = "Google::Protobuf::RepeatedField[#{elem_type}]"
|
208
162
|
|
209
163
|
default_args = [descriptor.type.inspect]
|
210
|
-
default_args << elem_type if
|
164
|
+
default_args << elem_type if [:enum, :message].include?(descriptor.type)
|
211
165
|
|
212
166
|
Field.new(
|
213
167
|
name: descriptor.name,
|
214
168
|
type: type,
|
215
169
|
init_type: "T.any(#{type}, T::Array[#{elem_type}])",
|
216
|
-
default: "Google::Protobuf::RepeatedField.new(#{default_args.join(
|
170
|
+
default: "Google::Protobuf::RepeatedField.new(#{default_args.join(", ")})"
|
217
171
|
)
|
218
172
|
end
|
219
173
|
else
|
@@ -230,25 +184,21 @@ module Tapioca
|
|
230
184
|
|
231
185
|
sig do
|
232
186
|
params(
|
233
|
-
klass:
|
187
|
+
klass: RBI::Scope,
|
234
188
|
desc: Google::Protobuf::FieldDescriptor,
|
235
189
|
).returns(Field)
|
236
190
|
end
|
237
191
|
def create_descriptor_method(klass, desc)
|
238
192
|
field = field_of(desc)
|
239
193
|
|
240
|
-
create_method(
|
241
|
-
klass,
|
194
|
+
klass.create_method(
|
242
195
|
field.name,
|
243
196
|
return_type: field.type
|
244
197
|
)
|
245
198
|
|
246
|
-
create_method(
|
247
|
-
klass,
|
199
|
+
klass.create_method(
|
248
200
|
"#{field.name}=",
|
249
|
-
parameters: [
|
250
|
-
Parlour::RbiGenerator::Parameter.new("value", type: field.type),
|
251
|
-
],
|
201
|
+
parameters: [create_param("value", type: field.type)],
|
252
202
|
return_type: field.type
|
253
203
|
)
|
254
204
|
|
@@ -1,8 +1,6 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require "parlour"
|
5
|
-
|
6
4
|
begin
|
7
5
|
require "sidekiq"
|
8
6
|
rescue LoadError
|
@@ -45,37 +43,52 @@ module Tapioca
|
|
45
43
|
class SidekiqWorker < Base
|
46
44
|
extend T::Sig
|
47
45
|
|
48
|
-
sig { override.params(root:
|
46
|
+
sig { override.params(root: RBI::Tree, constant: T.class_of(::Sidekiq::Worker)).void }
|
49
47
|
def decorate(root, constant)
|
50
48
|
return unless constant.instance_methods.include?(:perform)
|
51
49
|
|
52
|
-
root.
|
50
|
+
root.create_path(constant) do |worker|
|
53
51
|
method_def = constant.instance_method(:perform)
|
54
52
|
|
55
|
-
async_params =
|
53
|
+
async_params = compile_method_parameters_to_rbi(method_def)
|
56
54
|
|
57
55
|
# `perform_at` and is just an alias for `perform_in` so both methods technically
|
58
56
|
# accept a datetime, time, or numeric but we're typing them differently so they
|
59
57
|
# semantically make sense.
|
60
58
|
at_params = [
|
61
|
-
|
59
|
+
create_param("interval", type: "T.any(DateTime, Time)"),
|
62
60
|
*async_params,
|
63
61
|
]
|
64
62
|
in_params = [
|
65
|
-
|
63
|
+
create_param("interval", type: "Numeric"),
|
66
64
|
*async_params,
|
67
65
|
]
|
68
66
|
|
69
|
-
|
70
|
-
|
71
|
-
|
67
|
+
generate_perform_method(constant, worker, "perform_async", async_params)
|
68
|
+
generate_perform_method(constant, worker, "perform_at", at_params)
|
69
|
+
generate_perform_method(constant, worker, "perform_in", in_params)
|
72
70
|
end
|
73
71
|
end
|
74
72
|
|
75
73
|
sig { override.returns(T::Enumerable[Module]) }
|
76
74
|
def gather_constants
|
77
|
-
|
78
|
-
|
75
|
+
all_classes.select { |c| c < Sidekiq::Worker }
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
sig do
|
81
|
+
params(
|
82
|
+
constant: T.class_of(::Sidekiq::Worker),
|
83
|
+
worker: RBI::Scope,
|
84
|
+
method_name: String,
|
85
|
+
parameters: T::Array[RBI::TypedParam]
|
86
|
+
).void
|
87
|
+
end
|
88
|
+
def generate_perform_method(constant, worker, method_name, parameters)
|
89
|
+
if constant.method(method_name.to_sym).owner == Sidekiq::Worker::ClassMethods
|
90
|
+
worker.create_method(method_name, parameters: parameters, return_type: "String", class_method: true)
|
91
|
+
end
|
79
92
|
end
|
80
93
|
end
|
81
94
|
end
|