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
2
validates_length_of :name
validates_length_of :name, :description
These are the currently available manual validations. 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:
Here we see an example of a class with both a manual and auto-validation declared:
1
2
3
4
5
6
7
8
9
10
11
12
require 'dm-validations'
class Account
include DataMapper::Resource
property :name, String
# good old fashioned manual validation
validates_length_of :name, :max => 20
property :content, Text, :length => 100..500
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.
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
2
3
4
5
6
7
8
my_account = Account.new(:name => 'Jose')
if my_account.save
# my_account is valid and has been saved
else
my_account.errors.each do |e|
puts e
end
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:
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.
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class WikiPage
include DataMapper::Resource
# properties ...
validates_with_method :check_citations
# checks that we've included at least 5 citations for our wikipage.
def check_citations
# in a 'real' example, the number of citations might be a property set by
# a before :valid? hook.
num = count_citations(self.body)
if num > 4
return true
else
[ false, "You must have at least #{5 - num} more citations for this article" ]
end
end
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:
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
2
3
4
5
6
7
8
9
10
11
class Ticket
include DataMapper::Resource
property :id, Serial
property :title, String, :required => true
property :description, Text
property :commit, String
property :status, Enum[ :new, :open, :invalid, :complete ]
validates_presence_of :commit, :if => lambda { |t| t.status == :complete }
end
The autovalidation that requires the title to be present will always run, but
the validates_presence_of
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.
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class Article
include DataMapper::Resource
property :id, Serial
property :title, String
property :picture_url, String
property :body, Text
property :published, Boolean
# validations
validates_presence_of :title, :when => [ :draft, :publish ]
validates_presence_of :picture_url, :when => [ :publish ]
validates_presence_of :body, :when => [ :draft, :publish ]
validates_length_of :body, :when => [ :publish ], :minimum => 1000
validates_absence_of :published, :when => [ :draft ]
end
# and now some results
@article = Article.new
@article.valid?(:draft)
# => false. We have no title, for a start.
@article.valid_for_publish?
# => false. We have no title, amongst many other issues.
# valid_for_publish? is provided shorthand for valid?(:publish)
# now set some properties
@article.title = 'DataMapper is awesome because ...'
@article.body = 'Well, where to begin ...'
@article.valid?(:draft)
# => true. We have a title, and a little body
@article.valid?(:publish)
# => false. Our body isn't long enough yet.
# save our article in the :draft context
@article.save(:draft)
# => true
# set some more properties
@article.picture_url = 'http://www.greatpictures.com/flower.jpg'
@article.body = an_essay_about_why_datamapper_rocks
@article.valid?(:draft)
# => true. Nothing wrong still
@article.valid?(:publish)
# => true. We have everything we need for a full article to be published!
@article.published = true
@article.save(:draft)
# => false. We set the published to true, so we can't save this as a draft.
# As long as our drafting method always saves with the :draft context, we won't ever
# accidentally save a half finished draft that the public will see.
@article.save(:publish)
# => true
# 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Article
include DataMapper::Resource
property :id, Serial
property :title, String, :required => true
property :permalink, String, :required => true
before :valid?, :set_permalink
# our callback needs to accept the context used in the validation,
# even if it ignores it, as #save calls #valid? with a context.
def set_permalink(context = :default)
self.permalink = title.gsub(/\s+/, '-')
end
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.