The Rails 5 attributes API is the best feature of Rails 5, but no one knows it yet. You hardly hear about it. It took me awhile to even find out how to use it.
In Rails 5, model attributes go through the attributes API when they are set from user input (or any setter) and retrieved from the database (or any getter). Rails has used an internal attributes API for it’s entire lifetime. When you set an integer field to “5”, it will be cast to 5. This API is now not only public, but can also be extended to support custom types that respond to a simple interface.
You’ve likely wished the attributes API has existed before. Anytime you’ve done something like this:
class
Item < ActiveRecord::Base
attr_accessor :price
before_validation :convert_price_to_price_cents
def price
price_cents.fdiv
100
end
def convert_price_to_price_cents
self.price_cents = price.to_f *
100
end
end
This is the example you will see most often, and it is a valid one (though the money-rails gem takes care of this particular case for us anyway).
There are other common use-cases for custom types, just off the top of my head:
- Converting phone number fields to e-164 format using a library like global_phone
- Generating referral codes, or other types of tokens
- Tracking “points” on a user, so that user.points returns Points.new(10) or whatever so that you can decorate 10 with additional points-specific methods
- Allowing encrypted attributes to be searchable using normal ActiveRecord syntax
- Returning null objects from missing associations
You can also use it with built in types. Let’s say you have a freeform textbox that gets converted into a date. You can define that attribute as a date and take advantage of Rails’s built-in date parsing.
Using the Attributes API
In Rails 5, ActiveRecord has an attribute class method. You should use this instead of the normal attr_accessor when defining even virtual attributes. The first argument is the field name, the second argument is the type.
class
Item
attribute :price, :integer
end
Integer here is the built in type. For custom types, give it an instance of your custom object. I’m going to use a very simple custom type for this example, called Inquiry, which returns a StringInquirer object for a string field.
class
Repository
attribute :branch, Inquiry.
new
end
You should inherit from ActiveRecord::Type::Value or a more specific type, such as ActiveRecord::Type::String which I will do here, since for all other purposes, it’s a string. I will also override the type method, although at the time of this writing I have no idea what it does and there is no documentation for it.
class
Inquiry < ActiveRecord::Type::String
def type
:inquiry
end
end
Override cast to return what you want. The default behavior is to call cast on both setting and getting. If you only want to do setting, override deserialize instead for the getting and leave cast for setting. By default, deserialize just calls cast.
class
Inquiry < ActiveRecord::Type::String
# ...
def cast(value)
super
.inquiry
end
end
If you are wondering, #inquiry and StringInquirer are built into Rails. It’s what you get from Rails.env, it basically allows you call predicate methods that return true if the string matches the method name.
That’s it. Now when I set branch, it will cast it to a StringInquirer. When I get branch, it will cast it to a StringInquirer.
repo = Repository.
new
repo.branch =
'master'
repo.branch.master?
#=>
true
Default Values
You can set defaults, too:
attribute :branch, Inquiry.
new
,
default
:
'develop'
Querying
As I mentioned in the encryption example, this API supports where. ActiveRecord will serialize any value being queried by calling serialize on your custom type. The type passed into serialize will be the one from cast/deserialize, so in this example, a StringInquiry. Note that in this case it is not necessary to define this method since I’m inheriting from a built-in type.
class
Inquiry < ActiveRecord::Type::String
# ...
def serialize(value) # value here is a StringInquiry
value.to_s
end
end