Write Software, Well

March 23, 2023

Understanding the Attribute Assignment API in Rails

Here's a regular Ruby class with two properties and a constructor. To create a new instance of the class, you have to pass the arguments expected by the constructor, in the same order.

class Person
attr_accessor :name, :age

def initialize(name, age)
@name = name
@age = age
end
end

# create a new object
akshay = Person.new('Akshay', 31)

However, if you've worked with ActiveRecord models in Rails, you must have noticed that they don't include the initialize method. Still, you can create them using the new method, passing in a hash of attributes in any order.

class Post < ApplicationRecord
end

# create a new post
post = Post.new(title: 'hello world', body: 'how are you?')

If you try this with a plain Ruby class, it throws an error.

akshay = Person.new(name: 'Akshay', age: 31)

# wrong number of arguments (given 1, expected 2) (ArgumentError)

So the question is: How does Rails make it work? 🤔

The AttributeAssignment Module

The answer lies in the ActiveModel::AttributeAssignment module, which includes the assign_atributes method. It allows you to set all the attributes by passing in a hash of attributes. The hash keys must match the attribute names.

class Language
include ActiveModel::AttributeAssignment
attr_accessor :title, :author
end

ruby = Language.new
ruby.assign_attributes(title: "Ruby", author: "Matz")

ruby.title # => 'Ruby'
ruby.author # => 'Matz'

ruby.assign_attributes(author: "Yukihiro Matsumoto")

ruby.title # => 'Ruby'
ruby.author # => 'Yukihiro Matsumoto'

Under the hood, the assign_attributes method calls the assign_attribute method for each key-value pair.

Here's the internal implementation of this method.

def _assign_attribute(k, v)
setter = :"#{k}="
if respond_to?(setter)
public_send(setter, v)
else
raise UnknownAttributeError.new(self, k.to_s)
end
end

Let's try to understand what's going on when we pass name: 'Akshay' when initializing the object.

  1. The setter will be set to :name=
  2. The respond_to? method checks if our class includes the name= method, which sets the name. If it does, it calls that method.
  3. Since our class uses the attr_accessor :name directive, the name= method is present.  Hence it calls the setter method, setting the value 'Akshay' for the name attribute.
  4. For attributes not defined via attr_accessor , attr_writer or via overridden methods, it raises the UnknownAttributeError.

That's nice, but how can you create new objects?

On its own, the AttributeAssignment module won't allow you to call the new method to create new objects. For that, you need the ActiveModel::API module.

The API module includes the AttributeAssignment module and provides an initialize method. This method takes an attributes hash as argument and calls the assign_attributes method, passing the hash.

module ActiveModel
module API
include ActiveModel::AttributeAssignment

def initialize(attributes = {})
assign_attributes(attributes) if attributes
super()
end
end
end

If you include the ActiveModel::API module in your plain Ruby classes and remove the constructor, you can create new instances like Active Record models.

class Person
include ActiveModel::API

attr_accessor :name, :age
end

# akshay = Person.new(name: 'Akshay', age: 31)
=> #<Person:0x000000010d045b60 @age=31, @name="Akshay">

How Do Rails Models Get This Behavior?

All Active Record models inherit from the ApplicationRecord class, which inherits from the ActiveRecord::Base class. The Base class includes the ActiveRecord::AttributeAssignment module. Finally, this module includes the ActiveModel::AttributeAssignment module.

Attribute Assignment

Now, the ActiveRecord::AttributeAssignment module provides its own internal implementation for the private methods, but that's a topic for another blog post.

I hope this post helped you gain a deeper understanding of the AttributeAssignment module. You can use it with plain Ruby classes to simplify your code. You can even use this feature outside Rails by only using the ActiveModel framework and including the ActiveModel::API module.

Subscribe to get future posts via email (or grab the RSS feed)

Comments

No comments yet. Be the first to leave one.

Sign in to leave a comment.