If there is no built-in validator that fits your needs you can always use a custom method to perform the validation.
This approach becomes troublesome if you need to apply the same validation to multiple attributes or models. This is where we can use a custom validator.
Custom Validators
Custom validators usually inherit from the ActiveModel::EachValidator class and define a validate_each method. I usually put all validators in /app/validators.
The syntax for using our custom validator is the same as for the built-in validators.
The validate_each method is provided with the model instance, the name of attribute being validated and the value of the attribute. We also have access to the slightly confusing options hash which contains optional parameters passed to the validator. In this case it would contain { supported_extensions: %w{.jpg .png .jpeg .tif} }.
Implementing the logic for the validator is now pretty straightforward.
By default this code will validate that the extension is either .jpg, .png or .jpeg - but allows this list of valid extensions to be customized as we’re doing in our example.
Note that I’m passing a symbol - :invalid_extension - to the errors object, instead of specifying an error message. This means we can specify the actual error message in our locales file instead of hardcoding the error message. I am also supplying the valid extensions as additional details that can be used by the error message.
Rails also allows us to easily test custom validators, which will also help to illustrate how our validator behaves.
Of course, when testing a validator we need some kind of model to test against. Instead of coupling our validator to an existing model it would be ideal if we could generate some kind of stub model. While we can simply write a class in our test - see this approach for an example - I prefer to use the with_model gem to do the boilerplate for me. I’m also using the shoulda-matchers gem that allows for succinct syntax.
Extending Built-in Validators
Many of the built-in validations allow us to specify many different options to get the exact validation behavior we want. For example, if we are storing the square footage of a property we would want to validate that the attribute is a positive integer.
However, if we need the same validation on multiple attributes or models it becomes troublesome to duplicate this validation. For core application logic it makes sense to wrap this in a custom validator that simply captures the custom options we pass to the built-in validator.
We can use the same pattern to test our validator.