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 endend
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, :integerendInteger 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.newendYou 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 endendOverride 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 endendIf 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.newrepo.branch = 'master' repo.branch.master?#=> trueDefault 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 endend