Yehuda Katz is a member of the Ember.js, Ruby on Rails and jQuery Core Teams; he spends his daytime hours at the startup he founded, Tilde Inc.. Yehuda is co-author of best-selling jQuery in Action and Rails 3 in Action. He spends most of his time hacking on open source—his main projects, like Thor, Handlebars and Janus—or traveling the world doing evangelism work. He can be found on Twitter as @wycats and on Github.

ActiveModel: Make Any Ruby Object Feel Like ActiveRecord

Rails 2.3 has a ton of really nice functionality locked up in monolithic components. I’ve posted quite a bit about how we’ve opened up a lot of that functionality in ActionPack, making it easier to reuse the router, dispatcher, and individual parts of ActionController. ActiveModel is another way we’ve exposed useful functionality to you in Rails 3.

Before I Begin, The ActiveModel API

Before I begin, there are two major elements to ActiveModel. The first is the ActiveModel API, the interface that models must adhere to in order to gain compatibility with ActionPack’s helpers. I’ll be talking more about that soon, but for now, the important thing about the ActiveModel API is that your models can become ActiveModel compliant without using a single line of Rails code.

In order to help you ensure that your models are compliant, ActiveModel comes with a module called ActiveModel::Lint that you can include into your test cases to test compliance with the API:

class LintTest < ActiveModel::TestCase
  include ActiveModel::Lint::Tests
 
  class CompliantModel
    extend ActiveModel::Naming
 
    def to_model
      self
    end
 
    def valid?()      true end
    def new_record?() true end
    def destroyed?()  true end
 
    def errors
      obj = Object.new
      def obj.[](key)         [] end
      def obj.full_messages() [] end
      obj
    end
  end
 
  def setup
    @model = CompliantModel.new
  end
end

The ActiveModel::Lint::Tests provide a series of tests that are run against the @model, testing for compliance.

ActiveModel Modules

The second interesting part of ActiveModel is a series of modules provided by ActiveModel that you can use to implement common model functionality on your own Ruby objects. These modules were extracted from ActiveRecord, and are now included in ActiveRecord.

Because we’re dogfooding these modules, you can be assured that APIs you bring in to your models will remain consistent with ActiveRecord, and that they’ll continue to be maintained in future releases of Rails.

The ActiveModel comes with internationalization baked in, providing an avenue for much better community sharing around translating error messages and the like.

The Validations System

This was perhaps the most frustrating coupling in ActiveRecord, because it meant that people writing libraries for, say, CouchDB had to choose between painstakingly copying the API over, allowing inconsistencies to creep in, or just inventing a whole new API.

Validations have a few different elements.

First, declaring the validations themselves. You’ve seen the usage before in ActiveRecord:

class Person < ActiveRecord::Base
  validates_presence_of :first_name, :last_name
end

To do the same thing for a plain old Ruby object, simply do the following:

class Person
  include ActiveModel::Validations
 
  validates_presence_of :first_name, :last_name
 
  attr_accessor :first_name, :last_name
  def initialize(first_name, last_name)
    @first_name, @last_name = first_name, last_name
  end
end

The validations system calls read_attribute_for_validation to get the attribute, but by default, it aliases that method to send, which supports the standard Ruby attribute system of attr_accessor.

To use a more custom attribute lookup, you can do:

class Person
  include ActiveModel::Validations
 
  validates_presence_of :first_name, :last_name
 
  def initialize(attributes = {})
    @attributes = attributes
  end
 
  def read_attribute_for_validation(key)
    @attributes[key]
  end
end

Let’s look at what a validator actually is. First of all, the validates_presence_of method:

def validates_presence_of(*attr_names)
  validates_with PresenceValidator, _merge_attributes(attr_names)
end

You can see that validates_presence_of is using the more primitive validates_with, passing it the validator class, merging in {:attributes => attribute_names} into the options passed to the validator. Next, the validator itself:

class PresenceValidator < EachValidator
  def validate(record)
    record.errors.add_on_blank(attributes, options[:message])
  end
end

The EachValidator that it inherits from validates each attribute with the validate method. In this case, it adds the error message to the record, only if the attribute is blank.

The add_on_blank method does add(attribute, :blank, :default => custom_message) if value.blank? (among other things), which is adding the localized :blank message to the object. If you take a look at the built-in locale/en.yml looks like:

en:
  errors:
    # The default format use in full error messages.
    format: "{{attribute}} {{message}}"
 
    # The values :model, :attribute and :value are always available for interpolation
    # The value :count is available when applicable. Can be used for pluralization.
    messages:
      inclusion: "is not included in the list"
      exclusion: "is reserved"
      invalid: "is invalid"
      confirmation: "doesn't match confirmation"
      accepted: "must be accepted"
      empty: "can't be empty"
      blank: "can't be blank"
      too_long: "is too long (maximum is {{count}} characters)"
      too_short: "is too short (minimum is {{count}} characters)"
      wrong_length: "is the wrong length (should be {{count}} characters)"
      not_a_number: "is not a number"
      greater_than: "must be greater than {{count}}"
      greater_than_or_equal_to: "must be greater than or equal to {{count}}"
      equal_to: "must be equal to {{count}}"
      less_than: "must be less than {{count}}"
      less_than_or_equal_to: "must be less than or equal to {{count}}"
      odd: "must be odd"
      even: "must be even"

As a result, the error message will read first_name can't be blank.

The Error object is also a part of ActiveModel.

Serialization

ActiveRecord also comes with default serialization for JSON and XML, allowing you to do things like: @person.to_json(:except => :comment).

The main important part of the serialization support is adding general support for specifying the attributes to include across all serializers. That means that you can do @person.to_xml(:except => :comment) as well.

To add serialization support to your own model, you will need to include the serialization module and implement attributes. Check it out:

class Person
  include ActiveModel::Serialization
 
  attr_accessor :attributes
  def initialize(attributes)
    @attributes = attributes
  end
end
 
p = Person.new(:first_name => "Yukihiro", :last_name => "Matsumoto")
p.to_json #=> %|{"first_name": "Yukihiro", "last_name": "Matsumoto"}|
p.to_json(:only => :first_name) #=> %|{"first_name": "Yukihiro"}|

You can also pass in a :methods option to specify methods to call for certain attributes that are determined dynamically.

Here's the Person model with validations and serialization:

class Person
  include ActiveModel::Validations
  include ActiveModel::Serialization
 
  validates_presence_of :first_name, :last_name
 
  attr_accessor :attributes
  def initialize(attributes = {})
    @attributes = attributes
  end
 
  def read_attribute_for_validation(key)
    @attributes[key]
  end
end

Others

Those are just two of the modules available in ActiveModel. Some others include:

  • AttributeMethods: Makes it easy to add attributes that are set like table_name :foo
  • Callbacks: ActiveRecord-style lifecycle callbacks.
  • Dirty: Support for dirty tracking
  • Naming: Default implementations of model.model_name, which are used by ActionPack (for instance, when you do render :partial => model
  • Observing: ActiveRecord-style observers
  • StateMachine: A simple state-machine implementation for models
  • Translation: The core translation support

This mostly reflects the first step of ActiveRecord extractions done by Josh Peek for his Google Summer of Code project last summer. Over time, I expect to see more extractions from ActiveRecord and more abstractions built up around ActiveModel.

I also expect to see a community building up around things like adding new validators, translations, serializers and more, especially now that they can be reused not only in ActiveRecord, but in MongoMapper, Cassandra Object, and other ORMs that leverage ActiveModel's built-in modules.

24 Responses to “ActiveModel: Make Any Ruby Object Feel Like ActiveRecord”

Very exciting! Interesting to a StateMachine in there. Will that be included in the default ActiveRecord Rails 3.0 configurations?

Excellent post. Nice to be able to use these outside of Rails (like Sinatra).

AM#new_record? sounds odd for me. There is no records for example in a CouchRest or Mongo. Why not rename it to AM#new? or something more appropriate?

kinda cool, thanks for sharing

hrmm.

“…These modules were extracted from ActiveRecord, and are now included in ActiveRecord…”

Was that a lot of work?

Presumably ActiveResource will be also implement the ActiveModel API?

Yehuda,

your are a fan of datamapper, why activemodel can’t act as dm?

Can be beautiful see in AR:

property :id, Serial

Thanks for the post!

I think that ActiveModel ensures a minimal interface to which an ORM should adhere. IMHO it is reasonable to not force that the single point of thruth resides within the model (even if it may be the most logical place) as long as it is somewhere. In the AR case it is obtained by reflection on the database layer. So I like the actual design decision even if I prefer the datamapper way for declaring model properties.

Thanks for the info, Yehuda. It looks like it will be easier to introspect a model’s validations from the outside, then? This is something I keep wanting and then writing some nasty workaround for.

Thanks for the info, Yehuda. It looks like it will be easier to introspect a model’s validations from the outside, then? This is something I keep wanting and then writing some nasty workaround for.

Hello, very cool you did that finally.
However, I’ve got a question: how is ActiveModel being integrated with ActiveResource? It seems essential to me that one can build an ActiveResource that is ActiveModel compliant. Will ActiveResource be compliant by default, will it be somewhat rewritten to account for it when it’s enabled? Thank you for any clarification.

The reason I’m asking this is that I’m the creator of the OOOR ActiveResource wrapper over OpenERP business objects http://github.com/rvalyi/ooor (they also deal with relations and inheritance). We wanted ActiveModel for long to port directly into the Rails proxies the validations (and hopefully more) from OpenERP.

Great post Yehuda. How will validates_uniqueness_of work in terms it needs to go back to database to check that’s it unique? This was something that was problematic writing CouchFoo.

Woohoo! That stuff looks awesome. I especially like ActiveModel::Lint — it’s like code teaching you how to code!

This is some hot stuff, Yehuda. Awesome job.

Thanks for the post Yehuuda. Quick question. I really want to use this in authlogic. What would you recommend the best way to do this while also supporting previous versions of rails? My only thought is to include the module with the library and load it if needed. That’s what I’ve saw other libraries do when named_scopes was a new feature. Thanks again!

This looks perfect for something I might have to do for an upcoming project.

Dynamic Models based on data retrieved from an API. Would this be a reasonable way of doing it? creating a model based on the data and then eval’ing it to create an ActiveModel class?

You might want to update your post to reflect that StateMachine has been taken out… :(

I’m a rails rookie. I’m surprised there isn’t validation support for the built-in date/time types. (I’ll try this plugin
http://github.com/adzap/validates_timeliness/
)
It would be nice if rails came with more string types (email, url, etc) & numeric types (currency, distance, etc).

The lack of this ripples so the live-validations plugin doesn’t do this either.

Good writeup! But I’d like to add one thing.

In the example where you go over Serialization, you mention .to_xml but only demonstrate .to_json, which is included by default when you include ActiveModel::Serialization. To get to_xml to work, you need to include ActiveModel::Serializers::Xml in your model instead of ActiveModel::Serialization .. also you need to be sure that your @attributes hash has stringified keys, or to_xml will fail.

Here is the updated code, with a little bit extra to send attribute values to their proper setter methods.

class Person
include ActiveModel::Validations
include ActiveModel::Serializers::Xml

attr_accessor :first_name, :last_name, :attributes

def initialize(attributes={})
@attributes = attributes.stringify_keys.inject({}) do |x,y|
respond_to?(“#{y.first}=”) && send(“#{y.first}=”, y.last) ? x.merge(y.first => y.last) : x
end
end
end

Fantastic post Yehuda!

To follow up on Billy’s comment on to_xml, I found that I also had to add the model_name method to my class in order to get to_xml to work properly. For example:

def self.model_name
ActiveModel::Name.new()
end

If it did not do this, I would get the following error:
undefined method ‘model_name’ for :Class
from /activemodel-3.0.0/lib/active_model/serializers/xml.rb:94 in ‘serialize’

Quick follow up to my last comment…

An alternative to defining model_name on your model, you can simply extend ActiveModel::Naming.

Thanks for Sharing this post.

I have a doubt. How does the relationships work in rails 3?

In my rails-3 app, I have 2 models.
class User
has_many :posts
include ActiveModel::Validations
validates_presence_of :username
end

class Post
belongs_to :user
include ActiveModel::Validations
validates_presence_of :title
end

When I am running this:
@users = User.where(:activated => true).joins(:posts)

Log says Undefined Method ‘has_many’ for User:Class

Is it necessary to inherit ActiveRecord::Base ?

Any help would be highly appreciated.

Thanks
Puneet

Puneet,

You are getting an Undefined Method error for has_many because your models (User, Post) are not derived from ActiveRecord and hence they have no functionality concerning database associations. I would love to know an elegant way around this (only because I like working with the rails has/belongs_to, etc way of doing things) but until then I strictly keep an object member such as user_id or post_id to keep tabs on what User or Post I’m looking for. Hope this helps.

Correction

These modules were extracted from ActiveRecord, and are now included in
ActiveModel not (ActiveRecord).

Leave a Reply

Archives

Categories

Meta