I am a Sr. Software Developer at Oracle Cloud. The opinions expressed here are my own and not necessarily those of my employer.
Rails validators
Rails model validations are very important for ensuring data integrity. You usually start with really simple inline validates :name, presence: true
.
You can then implement more complex validations for inclusion, format, numericallity. Or add conditional validations. Eventually the logic gets too complex for one line statements and you write custom methods
class User < ApplicationRecord
validate :valid_user, on: :create
private
def valid_user
errors.add(:base, "is not valid") if ... # add biz logic here
end
end
But then you encounter a situation where you need to perform the same validations in two different models.
# app/models/user.rb
class NameValidator < ActiveModel::Validator
def validate(record)
unless record.name.starts_with? 'X'
record.errors[:name] << 'Need a name starting with X please!'
end
end
end
class User < ApplicationRecord
validates_with NameValidator
...
end
# app/models/organization.rb
class Organization < ApplicationRecord
validates_with User::NameValidator
...
end
However, you will get these warnings in your logs:
App 1206 stderr: .../app/models/organization.rb:43: warning: toplevel constant
NameValidator referenced by User::NameValidator
Plus this approach is just not very clean. Instead, why not create a app/validators folder and put the class there?
# app/validators/name_validator.rb
class NameValidator < ActiveModel::Validator
def validate(record)
@record = record
# => call appropriate method to do validation based on record type
send(record.class.name.downcase)
end
private
def user
unless @record.name.starts_with? 'X'
@record.errors[:name] << 'Need a name starting with X please!'
end
end
def organization
# could be slightly different validation
end
end
# app/models/user.rb
class User < ApplicationRecord
validates_with NameValidator
...
end
# app/models/organization.rb
class Organization < ApplicationRecord
validates_with NameValidator
...
end
Keep in mind that these validators are POROs so you could create class AppplicationValidator < ActiveModel::Validator
to contain common logic and inherit from that. And you can create spec/validators/name_validator_spec.rb
and test validation by passing different record types (user or organization).
# spec/validators/name_validator_spec.rb
require 'rails_helper'
describe NameValidator, type: :validator do
context 'user' do
it 'valid' do
user = build(:user, name: 'X org')
expect(user).to be_valid
end
it 'invalid' do
user = build(:user, name: 'wrong name')
expect(user).not_to be_valid
end
end
context 'organization' do
# similar tests
end
end
One useful place for custom validator class is when records can only belong to certain types of records. Let’s say we have Account that belongs_to
Organization. User also belongs_to
Organization AND has_and_belongs_to_many
Accounts. But User cannot have Account that belongs_to different Organization than User.
# app/models/
class User < ApplicationRecord
validates_with CommonUserOrgAccount
end
class Organization < ApplicationRecord
validates_with CommonUserOrgAccount
end
class Account < ApplicationRecord
validates_with CommonUserOrgAccount
end
# app/validators/common_user_org_account.rb
class CommonUserOrgAccount < ActiveModel::Validator
def validate(record)
@record = record
# => call appropriate method to do validation based on record type
send(record.class.name.downcase)
end
private
def user
# validation logic here
end
def organization
end
def account
end
end
# spec/validators/common_user_org_account_spec.rb
require 'rails_helper'
describe CommonUserOrgAccount, type: :validator do
context 'user'
context 'organization'
context 'account'
end
Useful links
- ActiveModel validations
- Mongoid validations if you are not using AR.