Class Macros. Let's create simplified version of Reform

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

Follow @makagon

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.

Follow @makagon