This post was published more than 3 years ago. It's likely the contents are obsolete or nostolgic. If you think the contents are worth updating, open an issue. Otherwise, sit back and enjoy the stroll down memory lane.
A Peek Behind ActiveModel::Model
I find myself using
ActiveModel::Model quite a bit.
It’s a quick and easy way to supercharge your ruby objects with all kinds of functionality to make them compatible with
ActionView (forms) and
Aside from this compatibility, it makes your ruby object feel a bit more like an
In fact, much of ActiveModel was extracted from ActiveRecord during the Rails 3 refactors of old.
I often reach for it when making form objects using The Command Pattern.
class CancelSubscription include ActiveModel::Model validates :subscription, :presence => true validates :cancellation_date, :presence => true attr_accessor :subscription, :cancellation_date def execute # logic for canceling a subscription end end form = CancelSubscription.new( subscription: subscription, cancellation_date: cancellation_date ) form.valid? # => true / false form.execute # => subscription cancelled!
Let’s take a closer look at
ActiveModel::Model to see what it’s doing for us behind the scenes.
It implements the following methods:
It includes these modules:
It is extended with these modules:
Each of these modules serves a purpose, but can also be used in isolation.
This module is included for the sole purpose of making this work:
form = CancelSubscription.new form.assign_attributes(subscription: subscription, cancellation_date: cancellation_date) form.subscription # => subscription form.cancellation_date # => cancellation_date
ActiveModel::Model#initialize method uses this functionality to implement the
ActiveRecord-like constructor interface:
form = CancelSubscription.new( subscription: subscription, cancellation_date: cancellation_date )
It iterates over the hash of parameters passed into the constructor, assigning values using the setters you’re expected to have defined via
If a setter hasn’t been defined, it will raise an
Validations define a rich class-level DSL for expressing what your model considers “valid”.
As a result, instances of the model gain a handful of methods allowing you to inquire about the validity (e.g
model.valid? has been called, it populates
model.errors with error messages indicating that your model has attribute values that the classes’ validators consider invalid.
model.errors method returns an instance of
ActiveModel::Error, which is a hash-like object for accessing attribute errors and messages.
This is extremely useful, especially when constructing models from user-provided data or data from programmer provided configuration.
form = CancelSubscription.new form.valid? # => false form.errors.full_messages # => ["subscription can't be blank", "cancellation_date can't be blank"]
Validations comes with a whole sweet of built-in validations - some of which you’ve probably used with
It also provides an easy way to define ad hoc validations via
validate as well as a full-featured
Validator interface for more re-usable validations.
As you may have guessed, this module is the glue between your model and the Rails i18n internationalization framework.
This module exposes a class level interface for defining translations that correspond to your attributes a la:
CancelSubscription.human_attribute_name(:cancellation_date) # => Fecha de cancelación
Also, it allows you to define an
i18n_scope method to control the expected i18n key path for your model’s translations.
Extending this module gives you a class method called
model_name that returns an instance of
Also, instances of your class will delegate the
model_name to your class.
ActiveModel::Name implements much of what powers Rails’ “convention over configuration” by taking the name of your class and hooking it up with
Understanding how this module works really pulls the curtain back from the infamous “Rails’ Magic”.
Continuing the “Rails Magic” misnomer,
Conversion is another common source of confusion.
It does the job of extracting data from your model to further inform rails conventions such as:
- Finding partial paths via
- Constructing URLs via
Worth noting that by default
to_param will return
nil unless your model is
However, you can certainly implement it yourself to your liking.
Putting It All Together
Hopefully, now you see that
ActiveModel::Model is nothing more than a series of modules meant to make your Ruby objects work more seamlessly with the Rails framework.
Once you realize what they do, it’s pretty easy to customize them or omit them entirely to suit your needs.
For example, a few things that bother me about
- I like to utilize keword args to fail fast.
- I dislike having to call super if I need to modify the constructor.
- I don’t like that calling code has access to setters from
module FormModel extend ActiveSupport::Concern include ActiveModel::Validations include ActiveModel::Conversion included do extend ActiveModel::Naming extend ActiveModel::Translation end def persisted? false end end
Now I can do something like this, opting out of the
AttributeAssignment behavior I don’t care for:
class CancelSubscription include FormModel validates :subscription, :presence => true validates :cancellation_date, :presence => true attr_reader :subscription, :cancellation_date def initialize(subscription:, cancellation_date: Time.zone.today) @subscription = subscription @cancellation_date = cancellation_date end def execute # logic for canceling a subscription end end
See a mistake? Kindly let me know by filing an issue