mirror of
https://github.com/github/rails.git
synced 2026-04-26 03:00:59 -04:00
This commit introduces two new methods that every
AMo compliant object must implement. Below are the
default implementations along with the implied
interface contract.
# Returns an Enumerable of all (primary) key
# attributes or nil if new_record? is true
def key
new_record? ? nil : [1]
end
# Returns a string representing the object's key
# suitable for use in URLs, or nil if new_record?
# is true
def to_param
key ? key.first.to_s : nil
end
1) The #key method
Previously rails' record_identifier code, which is
used in the #dom_id helper, relied on calling #id
on the record to provide a reasonable DOM id. Now
with rails3 being all ORM agnostic, it's not safe
anymore to assume that every record ever will have
an #id as its primary key attribute.
Having a #key method available on every AMo object
means that #dom_id can be implemented using
record.to_model.key # instead of
record.id
Using this we're able to take composite primary
keys into account (e.g. available in datamapper)
by implementing #dom_id using a newly added
record_key_for_dom_id(record)
method. The user can overwrite this method to
provide customized versions of the object's key
used in #dom_id.
Also, dealing with more complex keys that can
contain arbitrary strings, means that we need to
make sure that we only provide DOM ids that are
valid according to the spec. For this reason, this
patch sends the key provided through a newly added
sanitize_dom_id(candidate_id)
method, that makes sure we only produce valid HTML
The reason to not just add #dom_id to the AMo
interface was that it feels like providing a DOM
id should not be a model concern. Adding #dom_id
to the AMo interface would force these concern on
the model, while it's better left to be implemented
in a helper.
Now one could say the same is true for #to_param,
and actually I think that it doesn't really fit
into the model either, but it's used in AR and it's
a main part of integrating into the rails router.
This is different from #dom_id which is only used
in view helpers and can be implemented on top of a
semantically more meaningful method like #key.
2) The #to_param method
Since the rails router relies on #to_param to be
present, AR::Base implements it and returns the
id by default, allowing the user to overwrite the
method if desired.
Now with different ORMs integrating into rails,
every ORM railtie needs to implement it's own
#to_param implementation while already providing
code to be AMo compliant. Since the whole point of
AMo compliance seems to be to integrate any ORM
seamlessly into rails, it seems fair that all we
really need to do as another ORM, is to be AMo
compliant. By including #to_param into the official
interface, we can make sure that this code can be
centralized in the various AMo compliance layers,
and not be added separately by every ORM railtie.
3) All specs pass
= Active Model - defined interfaces for Rails
Prior to Rails 3.0, if a plugin or gem developer wanted to be able to have
an object interact with Action Pack helpers, it was required to either
copy chunks of code from Rails, or monkey patch entire helpers to make them
handle objects that did not look like Active Record. This generated code
duplication and fragile applications that broke on upgrades.
Active Model is a solution for this problem.
Active Model provides a known set of interfaces that your objects can implement
to then present a common interface to the Action Pack helpers. You can include
functionality from the following modules:
* Adding attribute magic to your objects
Add prefixes and suffixes to defined attribute methods...
class Person
include ActiveModel::AttributeMethods
attribute_method_prefix 'clear_'
define_attribute_methods [:name, :age]
attr_accessor :name, :age
def clear_attribute(attr)
send("#{attr}=", nil)
end
end
...gives you clear_name, clear_age.
{Learn more}[link:classes/ActiveModel/AttributeMethods.html]
* Adding callbacks to your objects
class Person
extend ActiveModel::Callbacks
define_model_callbacks :create
def create
_run_create_callbacks do
# Your create action methods here
end
end
end
...gives you before_create, around_create and after_create class methods that
wrap your create method.
{Learn more}[link:classes/ActiveModel/CallBacks.html]
* For classes that already look like an Active Record object
class Person
include ActiveModel::Conversion
end
...returns the class itself when sent :to_model
{Learn more}[link:classes/ActiveModel/Conversion.html]
* Tracking changes in your object
Provides all the value tracking features implemented by ActiveRecord...
person = Person.new
person.name # => nil
person.changed? # => false
person.name = 'bob'
person.changed? # => true
person.changed # => ['name']
person.changes # => { 'name' => [nil, 'bob'] }
person.name = 'robert'
person.save
person.previous_changes # => {'name' => ['bob, 'robert']}
{Learn more}[link:classes/ActiveModel/Dirty.html]
* Adding +errors+ support to your object
Provides the error messages to allow your object to interact with Action Pack
helpers seamlessly...
class Person
def initialize
@errors = ActiveModel::Errors.new(self)
end
attr_accessor :name
attr_reader :errors
def validate!
errors.add(:name, "can not be nil") if name == nil
end
def ErrorsPerson.human_attribute_name(attr, options = {})
"Name"
end
end
... gives you...
person.errors.full_messages
# => ["Name Can not be nil"]
person.errors.full_messages
# => ["Name Can not be nil"]
{Learn more}[link:classes/ActiveModel/Errors.html]
* Testing the compliance of your object
Use ActiveModel::Lint to test the compliance of your object to the
basic ActiveModel API...
{Learn more}[link:classes/ActiveModel/Lint/Tests.html]
* Providing a human face to your object
ActiveModel::Naming provides your model with the model_name convention
and a human_name attribute...
class NamedPerson
extend ActiveModel::Naming
end
...gives you...
NamedPerson.model_name #=> "NamedPerson"
NamedPerson.model_name.human #=> "Named person"
{Learn more}[link:classes/ActiveModel/Naming.html]
* Adding observer support to your objects
ActiveModel::Observers allows your object to implement the Observer
pattern in a Rails App and take advantage of all the standard observer
functions.
{Learn more}[link:classes/ActiveModel/Observer.html]
* Making your object serializable
ActiveModel::Serialization provides a standard interface for your object
to provide to_json or to_xml serialization...
s = SerialPerson.new
s.serializable_hash # => {"name"=>nil}
s.to_json # => "{\"name\":null}"
s.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
{Learn more}[link:classes/ActiveModel/Serialization.html]
* Turning your object into a finite State Machine
ActiveModel::StateMachine provides a clean way to include all the methods
you need to transform your object into a finite State Machine...
light = TrafficLight.new
light.current_state #=> :red
light.change_color! #=> true
light.current_state #=> :green
{Learn more}[link:classes/ActiveModel/StateMachine.html]
* Integrating with Rail's internationalization (i18n) handling through
ActiveModel::Translations...
class Person
extend ActiveModel::Translation
end
{Learn more}[link:classes/ActiveModel/Translation.html]
* Providing a full Validation stack for your objects...
class Person
include ActiveModel::Validations
attr_accessor :first_name, :last_name
validates_each :first_name, :last_name do |record, attr, value|
record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
end
end
person = Person.new(:first_name => 'zoolander')
person.valid? #=> false
{Learn more}[link:classes/ActiveModel/Validations.html]
* Make custom validators
class Person
include ActiveModel::Validations
validates_with HasNameValidator
attr_accessor :name
end
class HasNameValidator < ActiveModel::Validator
def validate(record)
record.errors[:name] = "must exist" if record.name.blank?
end
end
p = ValidatorPerson.new
p.valid? #=> false
p.errors.full_messages #=> ["Name must exist"]
p.name = "Bob"
p.valid? #=> true
{Learn more}[link:classes/ActiveModel/Validator.html]