Seven Useful ActiveModel Validators

Zachary Porter, Former Senior Developer

Article Category: #Code

Posted on

Custom ActiveModel::Validators are an easy way to validate individual attributes on your Rails models. All that's required is a Ruby class that inherits from ActiveModel::EachValidator and implements a validate_each method that takes three arguments: record, attribute, and value. I have written a few lately, so I pinged the rest of the amazingly talented Viget developers for some contributions. Here's what we came up with.

Simple URI Validator

A "simple URI" can be either a relative path or an absolute URL. In this case, any value that could be parsed by Ruby's URI module is allowed:

 class UriValidator < ActiveModel::EachValidator
 def validate_each(record, attribute, value)
 unless valid_uri?(value)
 record.errors[attribute] << (options[:message] || 'is not a valid URI')
 end
 end


 private

 def valid_uri?(uri)
 URI.parse(uri)
 true

 rescue URI::InvalidURIError
 false
 end
end

Full URL Validator

A "full URL" is defined as requiring a host and scheme. Ruby provides a regular expression to match against, so that's what is used in this validator:

 class FullUrlValidator < ActiveModel::EachValidator
 VALID_SCHEMES = %w(http https)
 
 def validate_each(record, attribute, value)
 unless value =~ URI::regexp(VALID_SCHEMES)
 record.errors[attribute] << (options[:message] || 'is not a valid URL')
 end
 end
end

The Ruby regular expression can be seen as too permissive. For a stricter regular expression, Brian Landau shared this Github gist.

Email Validator

My good friends Lawson Kurtz and Mike Ackerman contributed the following email address validator:

 class EmailValidator < ActiveModel::EachValidator
 def validate_each(record, attribute, value)
 unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
 record.errors[attribute] << (options[:message] || "is not a valid e-mail address")
 end
 end
end

If you'd rather validate by performing a DNS lookup, Brian Landau has you covered with this Github gist.

Secure Password Validator

Lawson provided this secure password validator (though credit goes to former Viget developer, James Cook):

 class SecurePasswordValidator < ActiveModel::EachValidator
 WORDS = YAML.load_file("config/bad_passwords.yml")

 def validate_each(record, attribute, value)
 if value.in?(WORDS)
 record.errors.add(attribute, "is a common password. Choose another.")
 end
 end
end

Twitter Handle Validator

Lawson supplied this validator that checks for valid Twitter handles:

 class TwitterHandleValidator < ActiveModel::EachValidator
 def validate_each(record, attribute, value)
 unless value =~ /^[A-Za-z0-9_]{1,15}$/
 record.errors[attribute] << (options[:message] || "is not a valid Twitter handle")
 end
 end
end

Hex Color Validator

A validator that's useful when an attribute should be a hex color value:

 class HexColorValidator < ActiveModel::EachValidator
 def validate_each(record, attribute, value)
 unless value =~ /\A([a-f0-9]{3}){,2}\z/i
 record.errors[attribute] << (options[:message] || 'is not a valid hex color value')
 end
 end
end

UPDATE: The regular expression has been simplified thanks to a comment from HappyNoff.

Regular Expression Validator

A great solution for attributes that should be a regular expression:

 class RegexpValidator < ActiveModel::EachValidator
 def validate_each(record, attribute, value)
 unless valid_regexp?(value)
 record.errors[attribute] << (options[:message] || 'is not a valid regular expression')
 end
 end


 private

 def valid_regexp?(value)
 Regexp.compile(value)
 true

 rescue RegexpError
 false
 end
end

Bonus Round

Replace all of those default error messages above with I18n translated strings for great justice. For the Regular Expression Validator above, the validate_each method could look something like this:

 def validate_each(record, attribute, value)
 unless valid_regexp?(value)
 default_message = record.errors.generate_message(attribute, :invalid_regexp)
 
 record.errors[attribute] << (options[:message] || default_message)
 end
end

Then the following could be added to config/locales/en.yml:

 en:
 errors:
 messages:
 invalid_regexp: is not a valid regular expression

Now the default error messages can be driven by I18n.

Conclusion

We've found these to be very helpful at Viget. What do you think? Which validators do you find useful? Are there others worth sharing? Please share in the comments below.

Related Articles