
Lets assume the predefined ActiveRecord like integer
or string
attributes are no enough for you.
For example you would like to have a money format, roman numeral, custom time format etc. The Rails 5 provide excellent interface just for this purpose.
To demonstrate this feature we will build a model of time tracker with a custom time entries attribute. Our goal is to be able to provide time with the following format: ad bh cm, where a, b and c are integers, and d, h, m means days, hours and minutes respectively (regular expression: ((\d)+d)?\s*((\d)+h)?\s*((\d)+m)?
). Eg. 3d 5h 1m means 3 days, 5 hours, 1 minute.
Lets start with a simple model TimeEntry
with only one integer field: spent
when we would like to store the spent time in minutes. The basic usage of custom attributes gives you power to change the attribute type or add default value eg:
1
2
3
4
|
class TimeEntry < ApplicationRecord
attribute :spent , :integer , default: 0
end
|
Changing attribute type in our case doesn’t make any sense but think about different usages. E.g. changing the amount of completed task from :decimal
to :integer
where you can image the task could be partially done.
But the power of the new interface shines when the attribute should be truly custom as in our example.
Lets add a custom attribute to our class:
1
2
3
4
|
class TimeEntry < ApplicationRecord
attribute :spent , :integer , default: 0
end
|
Now the attribute spent
has a :project_time
type. We have to define this attribute:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
class ProjectTime < ActiveRecord::Type:: Integer
def deserialize(value)
if value.is_a?( String )
to_minutes(value)
else
super
end
end
def serialize(value)
to_minutes(value)
end
private
def to_minutes(time)
time_sum = 0
time.split( ' ' ). each do |time_part|
value = time_part.to_i
type = time_part[- 1 , 1 ]
case type
when 'm'
when 'h'
value *= 60
when 'd'
value *= 8 * 60
else
value *= 60
end
time_sum += value
end
time_sum
end
end
|
Our attribute class must derive from one of the ActiveRecord types and implement method cast(value)
which transforms provided value to derived Active Record type. In our case we perform transformation only when the provided value is a String otherwise the default integer casting is performed. Private method to_minutes
converts formatted time to an integer representing spent minutes. I assumed that 1d = 8h = 480m. E.g. result of to_minutes('1d 1h 1m') = 541
The one final and obligatory step is to connect new :project_time
type with respective class ProjectType:
1
2
|
ActiveRecord::Type.register( :project_time , ProjectTime)
|
Now you can create time entries like that:
1
2
|
entry = TimeEntry.create(spent: '1d 1h 1m' )
entry.spent
|
But now thank to serialize
method we are also able to search:
1
2
3
|
TimeEntry.where(spent: '1d 1h 1m' ).count
TimeEntry.where(spent: '9h 1m' ).count
TimeEntry.where(spent: '1d 1h 2m' ).count
|
You can investigate this mechanism deeper on the API documentation page: http://edgeapi.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html