Class Macros. Let's create simplified version of Reform

Subscribe to receive new articles. No spam. Quality content.

If you use Rails you could see and use class macros many times. For example we use it for defining associations in ActiveRecord models:

class User
  has_many :posts
end

has_many it's a class macro.

Many gems provide own class macros, for example:

class Post
  acts_as_paranoid
end

acts_as_paranoid - that's another class macro.

Today we will learn how to create own class macros.

I was going to to write about class macros couple months ago, but now I feel that it's time. I've attended Trailblazer workshop where creator of Trailblazer Nick Sutterer showed us many cool features. That was really inspiring so I decided to write something similar to one of components of Trailblazer.

Reform - it's one of Trailblazer's gems. You can easy find out how to use it. I would describe it as a form object which allows us to define which params we want to accept from user. Also we can connect model to form object. Form is responsible for validation, filtering params and applying valid params to connected model.

Description might be a little bit hard to understand, so let's check the code. Today we're going to create mini-version of Reform gem. Let's see how our form object will look like when we implement it:

class MyForm < Proform
  attribute :title
  attribute :description

  validates :title, presence: true
end

We will inherit MyForm class from base class Proform (which we will implement later). We will be able to specify which attributes we're going to accept from user. Besides that we will specify validation rules: for example title is required for MyForm.

It will allow us to use form object this way:

# let's use Struct as a model for simplicity
Product = Struct.new(:title, :description)
my_product = Product.new('RubyBlog', 'Blog about Ruby and Ruby on Rails')

# we use params hash as a user's input
params = { description: 'Foo' }

# instantiate form object with my_product model
form = MyForm.new(my_product)
form.valid? # => true. valid by default

# validate form with params
form.validate(params) # => false, because title is required

form.valid? # => false
form.errors # {:title=>["can't be blank"]}

# let's pass valid params to form object
params = { title: 'rubyblog.pro' }
form = MyForm.new(my_product)
form.validate(params) # => true
form.valid? # => true
form.apply # assigns provided title to model my_product

I know that we have a lot of code in this example, but let's try to understand what we can do with this form object. In MyForm we declared that form will accept attributes title and description. Also we specified that title field is required.

Let's imagine that we have this model of data:

Product = Struct.new(:title, :description)
my_product = Product.new('RubyBlog', 'Blog about Ruby and Ruby on Rails')

For this example I used Struct, but in your application that could be ActiveRecord model or any other data object. All we need to do is pass model to form initialization:

form = MyForm.new(my_product)

Now we have our form object so we can pass any data we receive from user. Form will be able to validate that data using method validate. To check if form is valid we can use valid? method. In case of invalid form - errors will return description of each particular error. If everything's ok we can call apply method which will assign attributes of form object to model.

I hope that at this moment we understand what we expect from forms and we can move forward to the most interesting part - implementation of parent class Proform, which will allow us to specify attributes and validation rules for form objects.

Implementation of Proform with Class Macros

Now let's check our form code one more time:

class MyForm < Proform
  attribute :title
  attribute :description

  validates :title, presence: true
end

We see that we need to add two class macros for Proform: attribute and validates.

attribute will define which params we're going to accept. If we receive param that wasn't defined as an attribute - we will ignore it.

Also, we will need to add validates macro, which will allow us to configure validation for attribute. For simplicity we will implement just one type of validation: presence: true.

Let's start from the base class Proform:

class Proform
end

class MyForm < Proform
end

Let's try to add non-existing macro and see what happens:

class Proform
end

class MyForm < Proform
  attribute :title # => `<class:MyForm>': undefined method `attribute' for MyForm:Class (NoMethodError)
end

That makes sense. Because Ruby executes all code inside a class - it tried to call method attribute for MyForm. Inside MyForm class at the moment of execution self equals to MyForm. So we need to add class method attribute to make it working.

class Proform
  def self.attribute(name)
    puts name
  end
end

class MyForm < Proform
  attribute :title  # => title
end

Class Macro worked well this time because we defined that class method in parent class.

Now we have an interesting question: where should we store list of all forms' attributes? Macros is being called immediately, because Ruby executes code inside class. It means that at any moment of time MyForm should know its own set of attributes.

But self.attribute it's a class method. It knows nothing about future objects of that class. Having that we can't assign any instance variable. We can not use class variables as well because each form should have own unique set of attributes. For example:

class Proform
  @@attributes = []

  def self.attribute(name)
    @@attributes << name
  end

  def self.attributes
    @@attributes
  end
end

class MyForm < Proform
  attribute :title 
end

class FooForm < Proform
  attribute :foo
end

MyForm.attributes # => WRONG [:title, :foo] 

Class Proform can't use class variables because attributes defined in one of classes will be attributes for other classes.

I don't want to go too deep into this idea with class variables. To implement Class Macro we will use class instance variable. In Ruby almost everything is an object. The same with classes. Each class it's an object of class Class. If it's an instance of class Class - it may have own instance variables.

If you feel that you don't understand this concept - it's ok, because I had to read "Metaprogramming Ruby 2" book three times to get the idea. If you haven't read this book yet - you definitely should do that. Let's move forward and use class instance variables to store array of attributes which should support our form:

class Proform
  def self.attributes
    @attributes ||= []
  end

  def self.attribute(name)
    attributes << name
  end
end

class MyForm < Proform
  attribute :title 
  attribute :description
end

class FooForm < Proform
  attribute :foo
end

MyForm.attributes # => [:title, :description]
FooForm.attributes #=> [:foo]

It works well and now each particular form knows about own attributes! Now it's not that hard to add one more class macro:

class Proform
  def self.attributes
    @attributes ||= []
  end

  def self.validations
    @validations ||= {}
  end

  def self.attribute(name)
    attributes << name
  end

  def self.validates(attr, params)
    validations[attr] = params
  end
end

We've added two class methods. self.validations returns list of all validations and self.validates(attr, params) adds new validation rule.

Now when we have those class macros, MyForm class can have attributes and validations:

class MyForm < Proform
  attribute :title 
  validates :title, presence: true
end

MyForm.attributes # => [:title]
MyForm.validations #=> {:title=>{:presence=>true}}

Now we can define validation and attributes which will be processed by form.

Next, we're going to add methods: #errors, #valid?, #validate(params) and #apply

As I mentioned before, on form initialization we will accept a model which will be tied to form object. Let's add method initialize to Proform class and implement valid? method:

class Proform
  def self.attributes
    @attributes ||= []
  end

  def self.validations
    @validations ||= {}
  end

  def self.attribute(name)
    attributes << name
  end

  def self.validates(attr, params)
    validations[attr] = params
  end

  attr_reader :model, :errors

  def initialize(model)
    @model = model
    @errors = Hash.new { |h, k| h[k] = [] } 
  end

  def valid?
    errors.empty?
  end
end

If you're wondering why default values for Hash looks weird, you can read more about it here. Long story short: main idea to have empty array for each new key in that hash.

Let's implement one of the most important parts: validate method. As we agreed we will support only one type of validation: presence: true, which will check that param was passed to form object:

def validate(params)
  @params = params.keep_if { |attr| self.class.attributes.include?(attr) }

  self.class.validations.each do |attr, validations|
    validations.each do |type, value|
      if type == :presence && value == true
        errors[attr] << "can't be blank" unless params[attr]
      end
    end  
  end

  valid?
end

We could write that code better, but since we have just one type of validation it's good enough to make it working. Let's check what we have in that method:

@params = params.keep_if { |attr| self.class.attributes.include?(attr) }

This line allows us to filter params. We filter out params which are not declared as attribute for form. One important thing here that we can get an access to form attributes using self.class.attributes.

Then we go through all defined validations and validate each param using validates method.

As I said now we have just a basic validation for presence of param:

if type == :presence && value == true
  errors[attr] << "can't be blank" unless params[attr]
end

Definitely if we add more validation we would need to pull out this logic into separate module and don't do all checks inside code of loop. But I tried to keep this example as simple as possible.

Last method which we need to implement - #apply, which would assign all valid params to model. Implementation is pretty straightforward:

def apply
  return false unless valid?
  return true unless params

  params.each {|attr, val| model.public_send("#{attr}=", val)}
end

We return false if params are not valid. Also we return true if params are blank.

If we have valid attributes, we go through all of them and assign its values to model using public_send.

Let's recap all code that we've created today:

class Proform
  def self.attributes
    @attributes ||= []
  end

  def self.validations
    @validations ||= {}
  end

  def self.attribute(name)
    attributes << name
  end

  def self.validates(attr, params)
    validations[attr] = params
  end

  attr_reader :model, :errors, :params

  def initialize(model)
    @model = model
    @errors = Hash.new { |h, k| h[k] = [] } 
  end

  def validate(params)
    @params = params.keep_if { |attr| self.class.attributes.include?(attr) }

    self.class.validations.each do |attr, validations|
      validations.each do |type, value|
        if type == :presence && value == true
          errors[attr] << "can't be blank" unless params[attr]
        end
      end  
    end

    valid?
  end

  def valid?
    errors.empty?
  end

  def apply
    return false unless valid?
    return true unless params

    params.each {|attr, val| model.public_send("#{attr}=", val)}
  end
end

class MyForm < Proform
  attribute :title 
  attribute :description

  validates :title, presence: true
end

Let's use our fresh new class MyForm. We will consider two cases: with valid and invalid params:

Product = Struct.new(:title, :description)
model = Product.new('RubyBlog', 'Description') 
# => #<struct Product title="RubyBlog", description="Description">

form = MyForm.new(model)
# with invalid params
form.validate(description: 'Test') # => false
# results of validation appeared in errors hash
form.errors # => {:title=>["can't be blank"]}
form.apply # => false. Invalid params didn't change model, apply returns false
model # => #<struct Product title="RubyBlog", description="Description">

form = MyForm.new(model)
# with valid params
form.validate(title: 'http://rubyblog.pro', description: 'Blog about Ruby') # => true
form.valid? # => true
form.errors # => {}
# apply changed model and assigned new attributes
form.apply # {:title=>"http://rubyblog.pro", :description=>"Blog about Ruby"}
model # =>  #<struct Product title="http://rubyblog.pro", description="Blog about Ruby">

I've commented almost each important line in that example.

This post is relatively long, but I hope it was useful. Often when developers see such class macros they think that it's some sort of magic. But in fact there is no magic. It's not that hard to use such approach in own gem or application.

I'll really appreciate if you write couple words about this article in comments and share link with friends. Also, please, send me a message if you have better way to implement class macro. I'm always open to new ideas!

For those who still think about class instance variables - I would highly recommend to read "Metaprogramming Ruby 2" book. Every time I read that book I find something new for myself.

Thanks for reading!

Subscribe to receive new articles. No spam. Quality content.

Comments