Skip to content

Commit 61402d6

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

File tree

2 files changed

+115
-22
lines changed

2 files changed

+115
-22
lines changed

lib/tapioca/dsl/compilers/frozen_record.rb

Lines changed: 45 additions & 4 deletions
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

Lines changed: 70 additions & 18 deletions
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,11 +101,63 @@ 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
111163
sig { params(grain: Symbol).returns(String) }
@@ -161,67 +213,67 @@ class Student
161213
include FrozenRecordAttributeMethods
162214
163215
module FrozenRecordAttributeMethods
164-
sig { returns(Integer) }
216+
sig { returns(T.nilable(::Integer)) }
165217
def age; end
166218
167219
sig { returns(T::Boolean) }
168220
def age?; end
169221
170-
sig { returns(Date) }
222+
sig { returns(T.nilable(::Date)) }
171223
def birth_date; end
172224
173225
sig { returns(T::Boolean) }
174226
def birth_date?; end
175227
176-
sig { returns(Array) }
228+
sig { returns(T.nilable(::Array)) }
177229
def favourite_foods; end
178230
179231
sig { returns(T::Boolean) }
180232
def favourite_foods?; end
181233
182-
sig { returns(String) }
234+
sig { returns(T.nilable(::String)) }
183235
def first_name; end
184236
185237
sig { returns(T::Boolean) }
186238
def first_name?; end
187239
188-
sig { returns(Integer) }
240+
sig { returns(T.untyped) }
189241
def id; end
190242
191243
sig { returns(T::Boolean) }
192244
def id?; end
193245
194-
sig { returns(T::Boolean) }
246+
sig { returns(T.nilable(T::Boolean)) }
195247
def is_cool_person; end
196248
197249
sig { returns(T::Boolean) }
198250
def is_cool_person?; end
199251
200-
sig { returns(String) }
252+
sig { returns(T.nilable(::String)) }
201253
def last_name; end
202254
203255
sig { returns(T::Boolean) }
204256
def last_name?; end
205257
206-
sig { returns(String) }
258+
sig { returns(T.nilable(::String)) }
207259
def location; end
208260
209261
sig { returns(T::Boolean) }
210262
def location?; end
211263
212-
sig { returns(Symbol) }
264+
sig { returns(::Symbol) }
213265
def shirt_size; end
214266
215267
sig { returns(T::Boolean) }
216268
def shirt_size?; end
217269
218-
sig { returns(Hash) }
270+
sig { returns(T.nilable(::Hash)) }
219271
def skills; end
220272
221273
sig { returns(T::Boolean) }
222274
def skills?; end
223275
224-
sig { returns(Time) }
276+
sig { returns(T.nilable(::DateTime)) }
225277
def updated_at; end
226278
227279
sig { returns(T::Boolean) }
@@ -257,13 +309,13 @@ class Student
257309
extend GeneratedRelationMethods
258310
259311
module FrozenRecordAttributeMethods
260-
sig { returns(String) }
312+
sig { returns(T::untyped) }
261313
def course; end
262314
263315
sig { returns(T::Boolean) }
264316
def course?; end
265317
266-
sig { returns(Integer) }
318+
sig { returns(T::untyped) }
267319
def id; end
268320
269321
sig { returns(T::Boolean) }

0 commit comments

Comments
 (0)