Skip to content

Commit f435efb

Browse files
committed
Use ActiveModel::Attributes to define fields
1 parent 1e96598 commit f435efb

File tree

2 files changed

+116
-22
lines changed

2 files changed

+116
-22
lines changed

lib/tapioca/dsl/compilers/frozen_record.rb

+45-4
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,21 @@ def decorate
7272
attributes = constant.attributes
7373
return if attributes.empty?
7474

75-
instance = constant.first
76-
7775
root.create_path(constant) do |record|
7876
module_name = "FrozenRecordAttributeMethods"
7977

8078
record.create_module(module_name) do |mod|
8179
attributes.each do |attribute|
82-
return_type = instance.attributes[attribute].class.name
83-
return_type = "T::Boolean" if ["FalseClass", "TrueClass"].include?(return_type)
80+
return_type = "T::untyped"
81+
if constant.respond_to?(:attribute_types)
82+
attribute_type = T.let(
83+
T.unsafe(constant).attribute_types[attribute],
84+
ActiveModel::Type::Value
85+
)
86+
has_default = T.let(constant.default_attributes.key?(attribute), T::Boolean)
87+
return_type = type_for(attribute_type, has_default)
88+
end
89+
8490
mod.create_method("#{attribute}?", return_type: "T::Boolean")
8591
mod.create_method(attribute.to_s, return_type: return_type)
8692
end
@@ -99,6 +105,41 @@ def self.gather_constants
99105

100106
private
101107

108+
sig { params(attribute_type_value: ::ActiveModel::Type::Value, has_default: T::Boolean).returns(::String) }
109+
def type_for(attribute_type_value, has_default)
110+
type = case attribute_type_value
111+
when ActiveModel::Type::Boolean
112+
"T::Boolean"
113+
when ActiveModel::Type::Date
114+
"::Date"
115+
when ActiveModel::Type::DateTime, ActiveModel::Type::Time
116+
"::DateTime"
117+
when ActiveModel::Type::Decimal
118+
"::BigDecimal"
119+
when ActiveModel::Type::Float
120+
"::Float"
121+
when ActiveModel::Type::Integer
122+
"::Integer"
123+
when ActiveModel::Type::String
124+
"::String"
125+
else
126+
other_type = attribute_type_value.type
127+
case other_type
128+
when :array
129+
"::Array"
130+
when :hash
131+
"::Hash"
132+
when :symbol
133+
"::Symbol"
134+
else
135+
# we don't want untyped to be wrapped by T.nilable, so just return early
136+
return "T.untyped"
137+
end
138+
end
139+
140+
has_default ? type : as_nilable_type(type)
141+
end
142+
102143
sig { params(record: RBI::Scope).void }
103144
def decorate_scopes(record)
104145
scopes = T.unsafe(constant).__tapioca_scope_names

spec/tapioca/dsl/compilers/frozen_record_spec.rb

+71-18
Original file line numberDiff line numberDiff line change
@@ -73,19 +73,19 @@ class Student
7373
include FrozenRecordAttributeMethods
7474
7575
module FrozenRecordAttributeMethods
76-
sig { returns(String) }
76+
sig { returns(T::untyped) }
7777
def first_name; end
7878
7979
sig { returns(T::Boolean) }
8080
def first_name?; end
8181
82-
sig { returns(Integer) }
82+
sig { returns(T::untyped) }
8383
def id; end
8484
8585
sig { returns(T::Boolean) }
8686
def id?; end
8787
88-
sig { returns(String) }
88+
sig { returns(T::untyped) }
8989
def last_name; end
9090
9191
sig { returns(T::Boolean) }
@@ -101,13 +101,66 @@ def last_name?; end
101101
add_ruby_file("student.rb", <<~RUBY)
102102
# typed: strong
103103
104+
class ArrayOfType < ActiveModel::Type::Value
105+
attr_reader :element_type
106+
107+
def initialize(element_type:)
108+
super()
109+
@element_type = element_type
110+
end
111+
112+
def type
113+
:array
114+
end
115+
end
116+
117+
class HashOfType < ActiveModel::Type::Value
118+
attr_reader :key_type
119+
attr_reader :value_type
120+
121+
def initialize(key_type:, value_type:)
122+
super()
123+
@key_type = key_type
124+
@value_type = value_type
125+
end
126+
127+
def type
128+
:hash
129+
end
130+
end
131+
132+
class SymbolType < ActiveModel::Type::Value
133+
def type
134+
:symbol
135+
end
136+
end
137+
138+
ActiveModel::Type.register(:array_of_type, ArrayOfType)
139+
ActiveModel::Type.register(:hash_of_type, HashOfType)
140+
ActiveModel::Type.register(:symbol, SymbolType)
141+
104142
class Student < FrozenRecord::Base
105-
extend(T::Sig)
143+
extend T::Sig
144+
include ActiveModel::Attributes
145+
146+
# specifically missing the id field, should be untyped
147+
attribute :first_name, :string
148+
attribute :last_name, :string
149+
attribute :age, :integer
150+
attribute :location, :string
151+
attribute :is_cool_person, :boolean
152+
attribute :birth_date, :date
153+
attribute :updated_at, :time
154+
# custom attribute types
155+
attribute :favourite_foods, :array_of_type, element_type: :string
156+
attribute :skills, :hash_of_type, key_type: :symbol, value_type: :string
157+
# attribute with a default, shouldn't be nilable
158+
attribute :shirt_size, :symbol
106159
107160
self.base_path = __dir__
108-
109161
self.default_attributes = { shirt_size: :large }
110162
163+
# Explicit method, shouldn't be in the RBI output
111164
sig { params(grain: Symbol).returns(String) }
112165
def area(grain:)
113166
parts = location.split(',').map(&:strip)
@@ -161,67 +214,67 @@ class Student
161214
include FrozenRecordAttributeMethods
162215
163216
module FrozenRecordAttributeMethods
164-
sig { returns(Integer) }
217+
sig { returns(T.nilable(::Integer)) }
165218
def age; end
166219
167220
sig { returns(T::Boolean) }
168221
def age?; end
169222
170-
sig { returns(Date) }
223+
sig { returns(T.nilable(::Date)) }
171224
def birth_date; end
172225
173226
sig { returns(T::Boolean) }
174227
def birth_date?; end
175228
176-
sig { returns(Array) }
229+
sig { returns(T.nilable(::Array)) }
177230
def favourite_foods; end
178231
179232
sig { returns(T::Boolean) }
180233
def favourite_foods?; end
181234
182-
sig { returns(String) }
235+
sig { returns(T.nilable(::String)) }
183236
def first_name; end
184237
185238
sig { returns(T::Boolean) }
186239
def first_name?; end
187240
188-
sig { returns(Integer) }
241+
sig { returns(T.untyped) }
189242
def id; end
190243
191244
sig { returns(T::Boolean) }
192245
def id?; end
193246
194-
sig { returns(T::Boolean) }
247+
sig { returns(T.nilable(T::Boolean)) }
195248
def is_cool_person; end
196249
197250
sig { returns(T::Boolean) }
198251
def is_cool_person?; end
199252
200-
sig { returns(String) }
253+
sig { returns(T.nilable(::String)) }
201254
def last_name; end
202255
203256
sig { returns(T::Boolean) }
204257
def last_name?; end
205258
206-
sig { returns(String) }
259+
sig { returns(T.nilable(::String)) }
207260
def location; end
208261
209262
sig { returns(T::Boolean) }
210263
def location?; end
211264
212-
sig { returns(Symbol) }
265+
sig { returns(::Symbol) }
213266
def shirt_size; end
214267
215268
sig { returns(T::Boolean) }
216269
def shirt_size?; end
217270
218-
sig { returns(Hash) }
271+
sig { returns(T.nilable(::Hash)) }
219272
def skills; end
220273
221274
sig { returns(T::Boolean) }
222275
def skills?; end
223276
224-
sig { returns(Time) }
277+
sig { returns(T.nilable(::DateTime)) }
225278
def updated_at; end
226279
227280
sig { returns(T::Boolean) }
@@ -257,13 +310,13 @@ class Student
257310
extend GeneratedRelationMethods
258311
259312
module FrozenRecordAttributeMethods
260-
sig { returns(String) }
313+
sig { returns(T::untyped) }
261314
def course; end
262315
263316
sig { returns(T::Boolean) }
264317
def course?; end
265318
266-
sig { returns(Integer) }
319+
sig { returns(T::untyped) }
267320
def id; end
268321
269322
sig { returns(T::Boolean) }

0 commit comments

Comments
 (0)