Dynamic Model validations with Ruby on Rails

written by Raphael

Ruby on Rails 3 made dynamic validations really easy. Yet I feel like most people follow bad advice when implementing dynamic validations in rails. If you read posts on Stackoverflow you'll find something like this:

class Project < ActiveRecord::Base
  validates_presence_of :name, if: :name_should_be_present
  attr_accessor :name_should_be_present
end
# ...
class ProjectsController < ApplicationController
  def create
    @project = Project.new(params)
    @project.name_should_be_present = true
    if @project.save
      # snip
    else
      # snap
    end
  end
end

The more complex your validations get e.g. when implementing a multi-step wizard your model will get unreadable and most probably untestable as well. If you need access to your current_user inside your validations all hell will break loose (like Thread global variables).

While the above version works I opt for the slightly longer version which cleanly separates my custom validations: ActiveModel::Validator. I have to write a little more code which enables me to cleanly separate my custom per-case validation logic from the remaining persistence layer.

class Project < ActiveRecord::Base
  validate :instance_validations
  attr_accessor :custom_validator
  attr_accessor :custom_validator_options
  def instance_validations
    validates_with custom_validator, (custom_validator_options || {}) if custom_validator.present?
  end
end
# ...
class ProjectsController < ApplicationController
  def create
    @project = Project.new(params)

    @project.custom_validator = ProjectValidator
    @project.custom_validator_options = { current_user: current_user }
    if @project.save
      # snip
    else
      # snap
    end
  end
end
# ...
class ProjectValidator  < ActiveModel::Validator
  def validate(project)
    current_user = options[:current_user]

    if current_user.project_owner?
      unless project.name.present?
        project.errors[:name] << I18n.t('activerecord.errors.messages.blank', attribute: I18n.t(:"activerecord.attributes.project.name"))
      end
    end
  end
end

A word of warning: you might be tempted to write this inside your controller:

class ProjectsController < ApplicationController
  def create
    @project = Project.new(params)

    @project.validates_with ProjectValidator, { current_user: current_user }
    if @project.save
      # fail, save works because #valid? returned true
    else
      # snap
    end
  end
end

While the errors are added to your instance #valid? will still return true because the errors need to be present when evaluating the validation callback. If you know a working solution which does not require an attr_accessor I'd be grateful for sharing.


deploying with mina & current git revision

written by Raphael

At the time of writing mina does not support writing a REVISION file on deployment, like capistrano does by default. Mina also deletes the .git directory at the end of each deployment, leaving you no option to access your git log to retrieve the currently deployed revision (note that this might change in the future).

Luckily, most software that depends on your app running out of a git revisioned directory, like for example squash, support fallback modes which rely on your REVISION file existing somewhere.

So here's a simple work around to create a REVISION file on deployment:

# inside your deploy.rb, right after set :branch
set :deployed_revision, %x[git rev-parse #{branch}].strip

# snip …

task :deploy => :environment do
  deploy do
    invoke :'git:clone'
    invoke :'deploy:link_shared_paths'
    invoke :'bundle:install'
    invoke :'rails:assets_precompile'

    to :launch do
      queue "echo '#{deployed_revision}' > #{deploy_to}/#{current_path}/REVISION"
      queue "touch tmp/restart.txt"
    end
  end
end

This should work great for regular deployments.

Using Jenkins for your staging deployments?

The above example won't work out of the box if you are using Jenkins to deploy your application. Luckily, jenkins exports the current revision to your environment as GIT_REVISION.

Add the following right after set :deployed_revision:

if ENV['GIT_COMMIT'] != nil && ENV['GIT_COMMIT'] != ''
  set :deployed_revision, ENV['GIT_COMMIT']
end

That's it. Now you have a REVISION file inside your current directory. Easy!


Automate commands using AutomatedCommands

written by Raphael

Since DHH released commands some months ago I was thinking about how to properly automated this so one does not have to manually run test "unit" and similar every time you change a file.

The result is AutomatedCommands. A combination of a simple rake task and named pipes to communicate with your rails instance. So how does it work, exactly?

First commands: commands enables you to run your TestUnit tests from within your rails console, if you are running the console in test environment. This removes the need to launch rails, speeding up your tests and removing the need for tools like spork or zeus. Sadly it doesn't work with rspec and the likes because rspec defaults to using executing ruby in a separate process.

Next up: AutomatedCommands. When you run

$ bundle exec rake automated_commands:watch

from inside your rails application directory I'm doing two things:

  1. create a named pipe called test_pipe
  2. Listen.to changes inside your test directory

If you now start up a rails console in test mode and execute

irb(main):001:0> start_test_listener

I'm blocking the console thread, waiting for messages to arrive over the named pipe. Once you make changes to any of your test files your tests are going to be executed - instantly.

If you want to stop just press STRG + C and drop back into your rails console.

AutomatedCommands is far from perfect as it won't work on Windows and there are properly some issues with the current implementation, but those might be removed in future versions. For now, it just works.

Note aside: make sure to disable cache_classes inside your test.rb environment. Otherwise, only changes made to your tests are picked up, and you need to restart your rails console for changes in your controllers to have an effect:

config.cache_classes = false

Feedback is welcome & Happy hacking!