Validations
DataMapper validations allow you to vet data prior to saving to a database. To make validations available to your app you simply ’require "dm-validations"’ in your application. With DataMapper there are two different ways you can validate your classes’ properties.
Manual Validation
Much like a certain other Ruby ORM we can call validation methods directly by passing them a property name (or multiple property names) to validate against.
1 validates_length_of :name
2 validates_length_of :name, :description
These are the currently available manual validations available. Please refer to the API docs for more detailed information.
- validates_absence_of
- validates_acceptance_of
- validates_with_block
- validates_confirmation_of
- validates_format_of
- validates_length_of
- validates_with_method
- validates_numericality_of
- validates_primitive_type_of
- validates_presence_of
- validates_uniqueness_of
- validates_within
Auto-Validations
By adding triggers to your property definitions you can both define and validate your classes properties all in one fell swoop.
Triggers that generate validator creation:
# implicitly creates a validates_present
:required => true
:length => (1..n)
# implicitly creates a validates_length
:length => 20
:length => (1..20) # cant be null
:length => (0..20) # can be null
# :size is a synonym to :length
# implicitly creates a validates_format
:format => :email_address # more predefined regexes to come
:format => /\w+_\w+/
:format => lambda {|str| str }
:format => Proc.new { |str| str }
Here we see an example of a class with both a manual and auto-validation declared:
1 require 'dm-validations'
2
3 class Account
4 include DataMapper::Resource
5
6 property :name, String
7
8 # good old fashioned manual validation
9 validates_length_of :name, :max => 20
10
11 property :content, Text, :length => (100..500)
12 end
Validating
DataMapper validations, when included, alter the default save/create/update process for a model.
You may manually validate a resource using the valid? method, which will return true if the resource is valid, and false if it is invalid.
In addition to the valid? method, there is also an all_valid? method that recursively walks both the current object and its associated objects and returns a comprehensive true/false result for the entire walk. If anything returns false, all_valid? will return false
Working with Validation Errors
If your validators find errors in your model, they will populate the Validate::ValidationErrors object that is available through each of your models via calls to your model’s errors method.
1 my_account = Account.new(:name => "Jose")
2 if my_account.save
3 # my_account is valid and has been saved
4 else
5 my_account.errors.each do |e|
6 puts e
7 end
8 end
Error Messages
The error messages for validations provided by DataMapper are generally clear, and explain exactly what has gone wrong. If they’re not what you want though, they can be changed. This is done via providing a :message in the options hash, for example:
validates_uniqueness_of_ :title, :scope => :section_id,
:message => "There's already a page of that title in this section"
This example also demonstrates the use of the :scope option to only check the property’s uniqueness within a narrow scope. This object won’t be valid if another object with the same @section_id@ already has that title.
Something similar can be done for auto-validations, too, via setting :messages in the property options.
property :email, String, :required => true, :unique => true, :format => :email_address,
:messages => {
:presence => "We need your email address.",
:is_unique => "We already have that email.",
:format => "Doesn't look like an email address to me ..."
}
To set an error message on an arbitrary field of the model, DataMapper provides the add command.
1 @resource.errors.add(:title, "Doesn't mention DataMapper")
This is probably of most use in custom validations, so …
Custom Validations
DataMapper provides a number of validations for various common situations such as checking for the length or presence of strings, or that a number falls in a particular range. Often this is enough, especially when validations are combined together to check a field for a number of properties. For the situations where it isn’t, DataMapper provides a couple of methods: validates_with_block and validates_with_method. They’re very similar in operation, with one accepting a block as the argument and the other taking a symbol representing a method name.
The method or block performs the validation tests and then should return true if the resource is valid or false if it is invalid. If the resource isn’t valid instead of just returning false, an array containing false and an error message, such as [ false, "FAIL!" ] can be returned. This will add the message to the errors on the resource.
1 class WikiPage
2 include DataMapper::Resource
3
4 # properties ...
5
6 validates_with_method :check_citations
7
8 # checks that we've included at least 5 citations for our wikipage.
9 def check_citations
10 # in a 'real' example, the number of citations might be a property set by
11 # a before :valid? hook.
12 num = count_citations(self.body)
13 if num > 4
14 return true
15 else
16 [ false, "You must have at least #{5 - num} more citations for this article" ]
17 end
18 end
19 end
Instead of setting an error on the whole resource, you can set an error on an individual property by passing this as the first argument to validates_with_block or validates_with_method. To use the previous example, replacing line 5 with:
validates_with_method :body, :method => :check_citations
This would result in the citations error message being added to the error messages for the body, which might improve how it is presented to the user.
Conditional Validations
Validations don’t always have to be run. For example, an issue tracking system designed for git integration might require a commit identifier for the fix–but only for a ticket which is being set to ‘complete’. A new, open or invalid ticket, of course, doesn’t necessarily have one. To cope with this situation and others like it, DataMapper offers conditional validation, using the :if and :unless clauses on a validation.
:if and :unless take as their value a symbol representing a method name or a Proc. The associated validation will run only if (or unless) the method or Proc returns something which evaluates to true. The chosen method should take no arguments, whilst the Proc will be called with a single argument, the resource being validated.
1 class Ticket
2 include DataMapper::Resource
3
4 property :id, Serial
5 property :title, String, :required => true
6 property :description, Text
7 property :commit, String
8 property :status, Enum[ :new, :open, :invalid, :complete ]
9
10 validates_presence_of :commit, :if => Proc.new {|t| t.status == :complete }
11 end
The autovalidation that requires the title to be present will always run, but the validates_present on the commit hash will only run if the status is :complete. Another example might be a change summary that is only required if the resource is already there–‘initial commit’ is hardly an enlightening message.
validates_length_of :change_summary, :min => 10, :unless => :new_record?
Sometimes a simple on and off switch is not enough, and so …
Contextual Validations
DataMapper Validations also provide a means of grouping your validations into contexts. This enables you to run different sets of validations under different contexts. All validations are performed in a context, even the auto-validations. This context is the :default context. Unless you specify otherwise, any validations added will be added to the :default context and the valid? method checks all the validations in this context.
One example might be differing standards for saving a draft version of an article, compared with the full and ready to publish article. A published article has a title, a body of over 1000 characters, and a sidebar picture. A draft article just needs a title and some kind of body. The length and the sidebar picture we can supply later. There’s also a published property, which is used as part of queries to select articles for public display.
To set a context on a validation, we use the :when option. It might also be desirable to set :auto_validation => false on the properties concerned, especially if we’re messing with default validations.
1 class Article
2 include DataMapper::Resource
3
4 property :id, Serial
5 property :title, String
6 property :sidebar_picture_url, String
7 property :body, Text
8 property :published, Boolean
9
10 # validations
11 validates_presence_of :title, :when => [ :draft, :publish ]
12 validates_presence_of :sidebar_picture_url, :when => [ :publish ]
13 validates_presence_of :body, :when => [ :draft, :publish ]
14 validates_length_of :body, :minimum => 1000, :when => [ :publish ]
15 validates_absence_of :published, :when => [ :draft ]
16 end
17
18 # and now some results
19 @article = Article.new
20
21 @article.valid?(:draft)
22 #=> false. We have no title, for a start.
23
24 @article.valid_for_publish?
25 #=> false. We have no title, amongst many other issues.
26 # valid_for_publish? is provided shorthand for valid?(:publish)
27
28 # now set some properties
29 @article.title = "DataMapper is awesome because ..."
30 @article.body = "Well, where to begin ..."
31
32 @article.valid?(:draft)
33 #=> true. We have a title, and a little body
34
35 @article.valid?(:publish)
36 #=> false. Our body isn't long enough yet.
37
38 # save our article in the :draft context
39 @article.save(:draft)
40 #=> true
41
42 # set some more properties
43 @article.sidebar_picture_url = "http://www.greatpictures.com/flower.jpg"
44 @article.body = an_essay_about_why_datamapper_rocks
45
46 @article.valid?(:draft)
47 #=> true. Nothing wrong still
48
49 @article.valid?(:publish)
50 #=> true. We have everything we need for a full article to be published!
51
52 @article.published = true
53
54 @article.save(:draft)
55 #=> false. We set the published to true, so we can't save this as a draft.
56 # As long as our drafting method always saves with the :draft context, we won't ever
57 # accidentally save a half finished draft that the public will see.
58
59 @article.save(:publish)
60 #=> true
61 # we can save it just fine as a published article though.
That was a long example, but it shows how to set up validations in differing contexts and also how to save in a particular context. One thing to be careful of when saving in a context is to make sure that any database level constraints, such as a NOT NULL column definition in a database, are checked in that context, or a data-store error may ensue.
Setting Properties Before Validation
It is sometimes necessary to set properties before a resource is saved or validated. Perhaps a required property can have a default value set from other properties or derived from the environment. To set these properties, a before :valid? hook should be used.
1 class Article
2 include DataMapper::Resource
3
4 property :id, Serial
5 property :title, String, :required => true
6 property :permalink, String, :required => true
7
8 before :valid?, :set_permalink
9
10 # our callback needs to accept the context used in the validation,
11 # even if it ignores it, as #save calls #valid? with a context.
12 def set_permalink(context = :default)
13 self.permalink = title.gsub(/\s+/,'-')
14 end
15 end
Be careful not to save your resource in these kinds of methods, or your application will spin off into infinite trying to save your object while saving your object.