📜 ⬆️ ⬇️

ActiveModel: let any Ruby object feel like ActiveRecord

Yehuda Katz posted this entry on his blog on January 10, 2010.

A huge amount of really good functionality of Rails 2.3 is hidden in its monolithic components. I have already published several messages about how we have simplified the code of the router, the dispatcher and some parts of the ActionController, partially reorganizing the functionality of the ActionPack. ActiveModel is another module that appeared in Rails 3 after the reorganization of useful functionality.


For starters, the ActiveModel API


ActiveModel has two main elements. The first is an API, an interface to which models must conform to for compatibility with ActionPack helpers. Then I will tell you more about it, but first an important detail: your model can be made similar to ActiveModel without a single line of Rails-code.
')
To make sure that your models are suitable for this, ActiveModel offers the ActiveModel::Lint module for testing compatibility with the API - you just need to plug it in (to tackle) the test:

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

Tests of the ActiveModel::Lint::Tests module check compatibility of the @model object.

ActiveModel Modules


The second interesting part of ActiveModel is a set of modules for implementing standard functionality in your own models. The code for them was removed from ActiveRecord, and now they are separately included in it by themselves.

Since we ourselves use these modules, you can be sure that the API functions that you add to your models will remain compatible with ActiveRecord, and that they will be supported in future releases of Rails.

The built-in internationalization in ActiveModel gives the community ample opportunity to work on translating error messages and the like.

Validation system


Validation was probably one of the most disappointing places for ActiveRecord because people who wrote libraries, for example, for CouchDB, had to choose between literal rewriting of the API with the ability to introduce different inconsistencies in the rewriting process and inventing a completely new API.

There are several new elements in validation.

First, the declaration of the validation itself. Do you remember how it was before in ActiveRecord:
Copy Source | Copy HTML<br/> class Person < ActiveRecord::Base <br/> validates_presence_of :first_name, :last_name<br/> end <br/> Ruby, :<br/> <br/> class Person <br/> include ActiveModel::Validations<br/> <br/> validates_presence_of :first_name, :last_name<br/> <br/> attr_accessor :first_name, :last_name<br/> def initialize (first_name, last_name)<br/> @first_name, @last_name = first_name, last_name<br/> end <br/> end <br/>

The validation system calls read_attribute_for_validation to get the attribute, but by default it’s just an alias for send that supports the standard Ruby attribute system via attr_accessor .

To change the attribute search method, you can override read_attribute_for_validation :
Copy Source | Copy HTML<br/> class Person <br/> include ActiveModel::Validations<br/> <br/> validates_presence_of :first_name, :last_name<br/> <br/> def initialize (attributes = {})<br/> @attributes = attributes<br/> end <br/> <br/> def read_attribute_for_validation (key)<br/> @attributes[key]<br/> end <br/> end <br/>

Let's see what a validator is. First of all, the validates_presence_of method:
Copy Source | Copy HTML<br/> def validates_presence_of (*attr_names)<br/> validates_with PresenceValidator, _merge_attributes(attr_names)<br/> end <br/>

As you can see, validates_presence_of uses the more primitive validates_with , passing it the class of the validator and adding the key {:attributes => attribute_names} to attr_names . Next is the validator class itself:
Copy Source | Copy HTML<br/> class PresenceValidator < EachValidator<br/> def validate (record)<br/> record. errors .add_on_blank(attributes, options[:message])<br/> end <br/> end <br/>

The validate method in the EachValidator class validates each attribute. In this case, it is redefined and adds an error message to the object only if the attribute is empty.

The add_on_blank method calls add(attribute, :blank, :default => custom_message) add_on_blank add(attribute, :blank, :default => custom_message) if value.blank? (among other things), which adds a localized :blank message to the object. The built-in localization file for the English locale/en.yml as follows:

Copy Source | Copy HTML<br/>en:<br/> errors:<br/> # . <br/> format : "{{attribute}} {{message}}" <br/> <br/> # :model, :attribute :value <br/> # :count . . <br/> messages:<br/> inclusion: "is not included in the list" <br/> exclusion: "is reserved" <br/> invalid: "is invalid" <br/> confirmation: "doesn't match confirmation" <br/> accepted: "must be accepted" <br/> empty: "can't be empty" <br/> blank: "can't be blank" <br/> too_long: "is too long (maximum is {{count}} characters)" <br/> too_short: "is too short (minimum is {{count}} characters)" <br/> wrong_length: "is the wrong length (should be {{count}} characters)" <br/> not_a_number: "is not a number" <br/> greater_than: "must be greater than {{count}}" <br/> greater_than_or_equal_to: "must be greater than or equal to {{count}}" <br/> equal_to: "must be equal to {{count}}" <br/> less_than: "must be less than {{count}}" <br/> less_than_or_equal_to: "must be less than or equal to {{count}}" <br/> odd: "must be odd" <br/> even: "must be even" <br/>

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

The Error object is also part of the ActiveModel.

Serialization


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

The most important thing for serialization is to support a common set of attributes accepted by all serializers. That is, you can do @person.to_xml(:except => :comment) .

To add serialization support to your own model, you need to add (inject) the serialization module and the implementation of the attributes method. See:
Copy Source | Copy HTML<br/> class Person <br/> include ActiveModel::Serialization<br/> <br/> attr_accessor :attributes<br/> def initialize (attributes)<br/> @attributes = attributes<br/> end <br/> end <br/> <br/> p = Person . new (:first_name => "Yukihiro" , :last_name => "Matsumoto" )<br/> p .to_json #=> %|{"first_name": "Yukihiro", "last_name": "Matsumoto"}| <br/> p .to_json(:only => :first_name) #=> %|{"first_name": "Yukihiro"}| <br/>

In order for specific attributes to be converted by some methods, you can pass the option :methods ; these methods will then be invoked dynamically.

Here is the Person model with validation and serialization:
Copy Source | Copy HTML<br/> class Person <br/> include ActiveModel::Validations<br/> include ActiveModel::Serialization<br/> <br/> validates_presence_of :first_name, :last_name<br/> <br/> attr_accessor :attributes<br/> def initialize (attributes = {})<br/> @attributes = attributes<br/> end <br/> <br/> def read_attribute_for_validation (key)<br/> @attributes[key]<br/> end <br/> end <br/>

Other modules


We met only two ActiveModel modules. Briefly about the rest:

AttributeMethods : Simplifies adding class methods to control attributes of the type table_name :foo .
Callbacks : ActiveRecord style object life cycle callbacks.
Dirty : Support for dirty objects.
Naming : Default implementations of model.model_name used by ActionPack (for example, for render :partial => model ).
Observing : ActiveRecord Observers (Observers).
StateMachine : A simple implementation of a state machine.
Translation : Basic support for translations into other languages ​​(integration with the I18n internationalization framework).
Josh Peek reorganized the methods from ActiveRecord into separate modules as part of his project for Google Summer of Code last summer, and this is only the first step of the whole process. Over time, I expect to see more things highlighted from ActiveRecord and more abstractions around ActiveModel.

I also expect from the community validators, translations, serializers, etc., especially now that they can be used not only in ActiveRecord, but also in MongoMapper, Cassandra Object and other ORMs using ActiveModel modules.

Source: https://habr.com/ru/post/86938/


All Articles