18 April 2009 · About 5 minutes read

Rails: Writing DRY Custom Validators

It is well know that Rails can quickly validate user input using validators such as validates_presence_of. What happens, though, when you need to write your own custom validations, whilst keeping things DRY?

Recently, I found a couple of duplicate validations in my code, and decided to DRY them up and implement my own custom validators in ActiveRecord. This post will guide you through that process.

Defining the Validator

We’ll build a custom validator to check that a model’s date attribute is set to a future date/time. Later, we’ll add configuration options to the validator. Here’s an example of how the final code might be used:

```ruby# Validates that +event_date+ is in the future

validates_is_after :event_date

Validates that +ended_at+ is later than +started_at+

validates_is_after :ended_at, :after => :started_at```

Mixing into ActiveRecord

To keep the code DRY, our validator should be available to any ActiveRecord::Base instance, so we’ll re-open the class definition and add our code:

```ruby# config/initializers/validators.rb

ActiveRecord::Base.class_eval do

def self.validates_is_after(*attr_names)

# todo: validation code

end

end```

Note: Most of the example code I found online put custom validator code into the lib folder, but I had more success following the Rails Guides site and creating a file under config/initializers, in this case called validators.rb

This code opens the ActiveRecord::Base class declaration, allowing us to add the new class method. All this is possible thanks to Ruby’s flexible mixins.

The single parameter _attr_names uses the Ruby splat operator, examples of which can be found throughout Rails. In essence, the splat operator (_) lets us to pass any number of parameter values into the method, and packs them all into an array for us to use.

Building the Validator

Now that we’ve declared our validator, we need to write the validating code. First, though, it’s important to note the scope of our validation code. Remember that validates_is_after is a class method - so it has no access to the instance attributes, such as ends_at - that we want to validate. To get around this, Rails provides the validates_each block:

```ruby# config/initializers/validators.rb

ActiveRecord::Base.class_eval do

def self.validates_is_after(*attr_names)

validates_each(attr_names) do |record, attr_name, value|

  # validate each attribute

end

end

end```

The block within validates_each will run for each attribute supplied to validates_is_after. Inside the block, record represents the current model (instance of ActiveRecord::Base), attr_name is the name of the attribute that is being validated, and value is the value of the attribute.

Next, we add the code that will validate each attribute is set to a value later the current time:

```ruby# config/initializers/validators.rb

def self.validates_is_after(*attr_names)

validates_each(attr_names) do record, attr_name, value
unless value.nil?

  record.errors.add(attr_name, 'must be after current time') if value < Time.now

end

end

end

…```

We now have a working custom validator that you can call from any model object. Try adding a new model object with an :event_date attribute and adding the following validation:

```rubyclass Event < ActiveRecord::Base

validates_is_after :event_date

end

/script/console

e = Event.new

e.event_date = 1.days.ago

e.valid?

=> false```

Configuring the Validation

You can extend the validation method to take optional configuration parameters. For example, rather than just comparing against the current time, we might want the validator to check that a date is later than another given attribute, such as a :start_date.

This is achieved by making use of Rails’ <a>extract_options!</a> method. extract_options! returns the last item of an array if it is a Hash, otherwise it returns an empty Hash. Thus, we can supply any number of optional configuration values to the validator.

For now, add an optional configuration parameter called :after. If set, the comparison will be carried out against the value of :after rather than the current time. If is not set, :after will default to the current time:

```ruby# config/initializers/validators.rb

def self.validates_is_after(*attr_names)

# Set the default configuration

configuration = { :after => Time.now }

# Update defaults with any supplied configuration values

configuration.update(attr_names.extract_options!)

# Validate each attribute, passing in the configuration

validates_each(attr_names, configuration) do record, attr_name, value
unless value.nil?

  if configuration[:after].is_a?(Time)

    after_date = configuration[:after]

  else

    after_date = record.send(configuration[:after])

  end

  unless after_date.nil?

    record.errors.add(attr_name, 'must be after ' + configuration[:after].to_s) if value < after_date

  end

end

end

end

…```

Notice that we need to check if configuration[:after] is an instance of Time. If so, we compare the value with that of the current attribute. If configuration[:after] is not a Time, it should be another attribute, so we get its value from the record and compare against that.

We could, therefore, also supply :after with a time, e.g:

```rubyclass Event < ActiveRecord::Base

validates_is_after :ends_at, :after => 30.days.from_now

end```

I hope this post helps you to develop your own ActiveRecord validators. I’m writing these guides as I learn more about Rails, so as always please feel free to comment below. If you spot some code in need of improvement, let me know and I’ll update accordingly. Thanks!

Chris Blunt
Chris Blunt @cblunt
Chris is the founder of Plymouth Software. As well as code and business, he enjoys being a Dad, swimming, and the fine art of drinking tea.