georgepalmer-couch_foo 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,927 @@
1
+ module CouchFoo
2
+ # Raised by save! and create! when the record is invalid. Use the
3
+ # +record+ method to retrieve the record which did not validate.
4
+ # begin
5
+ # complex_operation_that_calls_save!_internally
6
+ # rescue CouchFoo::RecordInvalid => invalid
7
+ # puts invalid.record.errors
8
+ # end
9
+ class RecordInvalid < CouchFooError
10
+ attr_reader :record
11
+ def initialize(record)
12
+ @record = record
13
+ super("Validation failed: #{@record.errors.full_messages.join(", ")}")
14
+ end
15
+ end
16
+
17
+ # Couch Foo validation is reported to and from this object, which is used by Base#save to
18
+ # determine whether the object is in a valid state to be saved. See usage example in Validations.
19
+ class Errors
20
+ include Enumerable
21
+
22
+ def initialize(base) # :nodoc:
23
+ @base, @errors = base, {}
24
+ end
25
+
26
+ @@default_error_messages = {
27
+ :inclusion => "is not included in the list",
28
+ :exclusion => "is reserved",
29
+ :invalid => "is invalid",
30
+ :confirmation => "doesn't match confirmation",
31
+ :accepted => "must be accepted",
32
+ :empty => "can't be empty",
33
+ :blank => "can't be blank",
34
+ :too_long => "is too long (maximum is %d characters)",
35
+ :too_short => "is too short (minimum is %d characters)",
36
+ :wrong_length => "is the wrong length (should be %d characters)",
37
+ :taken => "has already been taken",
38
+ :not_a_number => "is not a number",
39
+ :greater_than => "must be greater than %d",
40
+ :greater_than_or_equal_to => "must be greater than or equal to %d",
41
+ :equal_to => "must be equal to %d",
42
+ :less_than => "must be less than %d",
43
+ :less_than_or_equal_to => "must be less than or equal to %d",
44
+ :odd => "must be odd",
45
+ :even => "must be even"
46
+ }
47
+
48
+ # Holds a hash with all the default error messages that can be replaced by your own copy or localizations.
49
+ cattr_accessor :default_error_messages
50
+
51
+ # Adds an error to the base object instead of any particular attribute. This is used
52
+ # to report errors that don't tie to any specific attribute, but rather to the object
53
+ # as a whole. These error messages don't get prepended with any field name when iterating
54
+ # with each_full, so they should be complete sentences.
55
+ def add_to_base(msg)
56
+ add(:base, msg)
57
+ end
58
+
59
+ # Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
60
+ # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>.
61
+ # More than one error can be added to the same +attribute+ in which case an array will be returned on a call
62
+ # to <tt>on(attribute)</tt>. If no +msg+ is supplied, "invalid" is assumed.
63
+ def add(attribute, msg = @@default_error_messages[:invalid])
64
+ @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
65
+ @errors[attribute.to_s] << msg
66
+ end
67
+
68
+ # Will add an error message to each of the attributes in +attributes+ that is empty.
69
+ def add_on_empty(attributes, msg = @@default_error_messages[:empty])
70
+ for attr in [attributes].flatten
71
+ value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
72
+ is_empty = value.respond_to?("empty?") ? value.empty? : false
73
+ add(attr, msg) unless !value.nil? && !is_empty
74
+ end
75
+ end
76
+
77
+ # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?).
78
+ def add_on_blank(attributes, msg = @@default_error_messages[:blank])
79
+ for attr in [attributes].flatten
80
+ value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
81
+ add(attr, msg) if value.blank?
82
+ end
83
+ end
84
+
85
+ # Returns true if the specified +attribute+ has errors associated with it.
86
+ #
87
+ # class Company < CouchFoo::Base
88
+ # validates_presence_of :name, :address, :email
89
+ # validates_length_of :name, :in => 5..30
90
+ # end
91
+ #
92
+ # company = Company.create(:address => '123 First St.')
93
+ # company.errors.invalid?(:name) # => true
94
+ # company.errors.invalid?(:address) # => false
95
+ def invalid?(attribute)
96
+ !@errors[attribute.to_s].nil?
97
+ end
98
+
99
+ # Returns nil, if no errors are associated with the specified +attribute+.
100
+ # Returns the error message, if one error is associated with the specified +attribute+.
101
+ # Returns an array of error messages, if more than one error is associated with the specified +attribute+.
102
+ #
103
+ # class Company < CouchFoo::Base
104
+ # validates_presence_of :name, :address, :email
105
+ # validates_length_of :name, :in => 5..30
106
+ # end
107
+ #
108
+ # company = Company.create(:address => '123 First St.')
109
+ # company.errors.on(:name) # => ["is too short (minimum is 5 characters)", "can't be blank"]
110
+ # company.errors.on(:email) # => "can't be blank"
111
+ # company.errors.on(:address) # => nil
112
+ def on(attribute)
113
+ errors = @errors[attribute.to_s]
114
+ return nil if errors.nil?
115
+ errors.size == 1 ? errors.first : errors
116
+ end
117
+
118
+ alias :[] :on
119
+
120
+ # Returns errors assigned to the base object through add_to_base according to the normal rules of on(attribute).
121
+ def on_base
122
+ on(:base)
123
+ end
124
+
125
+ # Yields each attribute and associated message per error added.
126
+ #
127
+ # class Company < CouchFoo::Base
128
+ # validates_presence_of :name, :address, :email
129
+ # validates_length_of :name, :in => 5..30
130
+ # end
131
+ #
132
+ # company = Company.create(:address => '123 First St.')
133
+ # company.errors.each{|attr,msg| puts "#{attr} - #{msg}" } # =>
134
+ # name - is too short (minimum is 5 characters)
135
+ # name - can't be blank
136
+ # address - can't be blank
137
+ def each
138
+ @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
139
+ end
140
+
141
+ # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
142
+ # through iteration as "First name can't be empty".
143
+ #
144
+ # class Company < CouchFoo::Base
145
+ # validates_presence_of :name, :address, :email
146
+ # validates_length_of :name, :in => 5..30
147
+ # end
148
+ #
149
+ # company = Company.create(:address => '123 First St.')
150
+ # company.errors.each_full{|msg| puts msg } # =>
151
+ # Name is too short (minimum is 5 characters)
152
+ # Name can't be blank
153
+ # Address can't be blank
154
+ def each_full
155
+ full_messages.each { |msg| yield msg }
156
+ end
157
+
158
+ # Returns all the full error messages in an array.
159
+ #
160
+ # class Company < CouchFoo::Base
161
+ # validates_presence_of :name, :address, :email
162
+ # validates_length_of :name, :in => 5..30
163
+ # end
164
+ #
165
+ # company = Company.create(:address => '123 First St.')
166
+ # company.errors.full_messages # =>
167
+ # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"]
168
+ def full_messages
169
+ full_messages = []
170
+
171
+ @errors.each_key do |attr|
172
+ @errors[attr].each do |msg|
173
+ next if msg.nil?
174
+
175
+ if attr == "base"
176
+ full_messages << msg
177
+ else
178
+ full_messages << @base.class.human_attribute_name(attr) + " " + msg
179
+ end
180
+ end
181
+ end
182
+ full_messages
183
+ end
184
+
185
+ # Returns true if no errors have been added.
186
+ def empty?
187
+ @errors.empty?
188
+ end
189
+
190
+ # Removes all errors that have been added.
191
+ def clear
192
+ @errors = {}
193
+ end
194
+
195
+ # Returns the total number of errors added. Two errors added to the same attribute will be counted as such.
196
+ def size
197
+ @errors.values.inject(0) { |error_count, attribute| error_count + attribute.size }
198
+ end
199
+
200
+ alias_method :count, :size
201
+ alias_method :length, :size
202
+
203
+ # Returns an XML representation of this error object.
204
+ #
205
+ # class Company < CouchFoo::Base
206
+ # validates_presence_of :name, :address, :email
207
+ # validates_length_of :name, :in => 5..30
208
+ # end
209
+ #
210
+ # company = Company.create(:address => '123 First St.')
211
+ # company.errors.to_xml # =>
212
+ # <?xml version="1.0" encoding="UTF-8"?>
213
+ # <errors>
214
+ # <error>Name is too short (minimum is 5 characters)</error>
215
+ # <error>Name can't be blank</error>
216
+ # <error>Address can't be blank</error>
217
+ # </errors>
218
+ def to_xml(options={})
219
+ options[:root] ||= "errors"
220
+ options[:indent] ||= 2
221
+ options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
222
+
223
+ options[:builder].instruct! unless options.delete(:skip_instruct)
224
+ options[:builder].errors do |e|
225
+ full_messages.each { |msg| e.error(msg) }
226
+ end
227
+ end
228
+ end
229
+
230
+
231
+ # Couch Foo implement validation by overwriting Base#validate (or the variations, +validate_on_create+
232
+ # and +validate_on_update+). Each of these methods can inspect the state of the object, which usually
233
+ # means ensuring that a number of attributes have a certain value (such as not empty, within a given
234
+ # range, matching a certain regular expression).
235
+ #
236
+ # Example:
237
+ #
238
+ # class Person < CouchFoo::Base
239
+ # protected
240
+ # def validate
241
+ # errors.add_on_empty %w( first_name last_name )
242
+ # errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
243
+ # end
244
+ #
245
+ # def validate_on_create # is only run the first time a new object is saved
246
+ # unless valid_discount?(membership_discount)
247
+ # errors.add("membership_discount", "has expired")
248
+ # end
249
+ # end
250
+ #
251
+ # def validate_on_update
252
+ # errors.add_to_base("No changes have occurred") if unchanged_attributes?
253
+ # end
254
+ # end
255
+ #
256
+ # person = Person.new("first_name" => "David", "phone_number" => "what?")
257
+ # person.save # => false (and doesn't do the save)
258
+ # person.errors.empty? # => false
259
+ # person.errors.count # => 2
260
+ # person.errors.on "last_name" # => "can't be empty"
261
+ # person.errors.on "phone_number" # => "has invalid format"
262
+ # person.errors.each_full { |msg| puts msg }
263
+ # # => "Last name can't be empty\n" +
264
+ # "Phone number has invalid format"
265
+ #
266
+ # person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" }
267
+ # person.save # => true (and person is now saved in the database)
268
+ #
269
+ # An Errors object is automatically created for every Couch Foo.
270
+ #
271
+ # Please do have a look at CouchFoo::Validations::ClassMethods for a higher level of validations.
272
+ module Validations
273
+ VALIDATIONS = %w( validate validate_on_create validate_on_update )
274
+
275
+ def self.included(base) # :nodoc:
276
+ base.extend ClassMethods
277
+ base.class_eval do
278
+ alias_method_chain :save, :validation
279
+ alias_method_chain :save!, :validation
280
+ alias_method_chain :update_attribute, :validation_skipping
281
+ end
282
+
283
+ base.send :include, ActiveSupport::Callbacks
284
+ base.define_callbacks *VALIDATIONS
285
+ end
286
+
287
+ # All of the following validations are defined in the class scope of the model that you're
288
+ # interested in validating. They offer a more declarative way of specifying when the model
289
+ # is valid and when it is not. It is recommended to use these over the low-level calls to
290
+ # +validate+ and +validate_on_create+ when possible.
291
+ module ClassMethods
292
+ DEFAULT_VALIDATION_OPTIONS = {
293
+ :on => :save,
294
+ :allow_nil => false,
295
+ :allow_blank => false,
296
+ :message => nil
297
+ }.freeze
298
+
299
+ ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze
300
+ ALL_NUMERICALITY_CHECKS = { :greater_than => '>', :greater_than_or_equal_to => '>=',
301
+ :equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=',
302
+ :odd => 'odd?', :even => 'even?' }.freeze
303
+
304
+ # Adds a validation method or block to the class. This is useful when
305
+ # overriding the +validate+ instance method becomes too unwieldly and
306
+ # you're looking for more descriptive declaration of your validations.
307
+ #
308
+ # This can be done with a symbol pointing to a method:
309
+ #
310
+ # class Comment < CouchFoo::Base
311
+ # validate :must_be_friends
312
+ #
313
+ # def must_be_friends
314
+ # errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee)
315
+ # end
316
+ # end
317
+ #
318
+ # Or with a block which is passed the current record to be validated:
319
+ #
320
+ # class Comment < CouchFoo::Base
321
+ # validate do |comment|
322
+ # comment.must_be_friends
323
+ # end
324
+ #
325
+ # def must_be_friends
326
+ # errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee)
327
+ # end
328
+ # end
329
+ #
330
+ # This usage applies to +validate_on_create+ and +validate_on_update+ as well.
331
+
332
+ # Validates each attribute against a block.
333
+ #
334
+ # class Person < CouchFoo::Base
335
+ # validates_each :first_name, :last_name do |record, attr, value|
336
+ # record.errors.add attr, 'starts with z.' if value[0] == ?z
337
+ # end
338
+ # end
339
+ #
340
+ # Options:
341
+ # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
342
+ # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
343
+ # * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
344
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
345
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
346
+ # method, proc or string should return or evaluate to a true or false value.
347
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
348
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
349
+ # method, proc or string should return or evaluate to a true or false value.
350
+ def validates_each(*attrs)
351
+ options = attrs.extract_options!.symbolize_keys
352
+ attrs = attrs.flatten
353
+
354
+ # Declare the validation.
355
+ send(validation_method(options[:on] || :save), options) do |record|
356
+ attrs.each do |attr|
357
+ value = record.send(attr)
358
+ next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
359
+ yield record, attr, value
360
+ end
361
+ end
362
+ end
363
+
364
+ # Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example:
365
+ #
366
+ # Model:
367
+ # class Person < CouchFoo::Base
368
+ # validates_confirmation_of :user_name, :password
369
+ # validates_confirmation_of :email_address, :message => "should match confirmation"
370
+ # end
371
+ #
372
+ # View:
373
+ # <%= password_field "person", "password" %>
374
+ # <%= password_field "person", "password_confirmation" %>
375
+ #
376
+ # The added +password_confirmation+ attribute is virtual; it exists only as an in-memory
377
+ # attribute for validating the password. To achieve this, the validation adds accessors to
378
+ # the model for the confirmation attribute. NOTE: This check is performed only if
379
+ # +password_confirmation+ is not +nil+, and by default only on save. To require confirmation,
380
+ # make sure to add a presence check for the confirmation attribute:
381
+ #
382
+ # validates_presence_of :password_confirmation, :if => :password_changed?
383
+ #
384
+ # Configuration options:
385
+ # * <tt>:message</tt> - A custom error message (default is: "doesn't match confirmation").
386
+ # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>,
387
+ # other options <tt>:create</tt>, <tt>:update</tt>).
388
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation
389
+ # should occur (e.g. <tt>:if => :allow_validation</tt>, or
390
+ # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc or string
391
+ # should return or evaluate to a true or false value.
392
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the
393
+ # validation should not occur (e.g. <tt>:unless => :skip_validation</tt>, or
394
+ # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, proc or
395
+ # string should return or evaluate to a true or false value.
396
+ def validates_confirmation_of(*attr_names)
397
+ configuration = { :message => CouchFoo::Errors.default_error_messages[:confirmation], :on => :save }
398
+ configuration.update(attr_names.extract_options!)
399
+
400
+ attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" }))
401
+
402
+ validates_each(attr_names, configuration) do |record, attr_name, value|
403
+ record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
404
+ end
405
+ end
406
+
407
+ # Encapsulates the pattern of wanting to validate the acceptance of a terms of service check
408
+ # box (or similar agreement). Example:
409
+ #
410
+ # class Person < CouchFoo::Base
411
+ # validates_acceptance_of :terms_of_service
412
+ # validates_acceptance_of :eula, :message => "must be abided"
413
+ # end
414
+ #
415
+ # If the database property does not exist, the +terms_of_service+ attribute is entirely virtual.
416
+ # This check is performed only if +terms_of_service+ is not +nil+ and by default on save.
417
+ #
418
+ # Configuration options:
419
+ # * <tt>:message</tt> - A custom error message (default is: "must be accepted").
420
+ # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>,
421
+ # other options <tt>:create</tt>, <tt>:update</tt>).
422
+ # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is true).
423
+ # * <tt>:accept</tt> - Specifies value that is considered accepted. The default value is a
424
+ # string "1", which makes it easy to relate to an HTML checkbox. This should be set to +true+
425
+ # if you are validating a database property, since the attribute is typecast from "1" to +true+
426
+ # before validation.
427
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation
428
+ # should occur (e.g. <tt>:if => :allow_validation</tt>, or
429
+ # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc or string
430
+ # should return or evaluate to a true or false value.
431
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation
432
+ # should not occur (e.g. <tt>:unless => :skip_validation</tt>, or
433
+ # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, proc or string
434
+ # should return or evaluate to a true or false value.
435
+ def validates_acceptance_of(*attr_names)
436
+ configuration = { :message => CouchFoo::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" }
437
+ configuration.update(attr_names.extract_options!)
438
+
439
+ names = attr_names.reject { |name| property_names.include?(name) }
440
+ attr_accessor(*names)
441
+
442
+ validates_each(attr_names,configuration) do |record, attr_name, value|
443
+ record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept]
444
+ end
445
+ end
446
+
447
+ # Validates that the specified attributes are not blank (as defined by Object#blank?). Happens by default on save. Example:
448
+ #
449
+ # class Person < CouchFoo::Base
450
+ # validates_presence_of :first_name
451
+ # end
452
+ #
453
+ # The first_name attribute must be in the object and it cannot be blank.
454
+ #
455
+ # If you want to validate the presence of a boolean field (where the real values are true and false),
456
+ # you will want to use validates_inclusion_of :field_name, :in => [true, false]
457
+ # This is due to the way Object#blank? handles boolean values. false.blank? # => true
458
+ #
459
+ # Configuration options:
460
+ # * <tt>message</tt> - A custom error message (default is: "can't be blank").
461
+ # * <tt>on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
462
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
463
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
464
+ # method, proc or string should return or evaluate to a true or false value.
465
+ # * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should
466
+ # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The
467
+ # method, proc or string should return or evaluate to a true or false value.
468
+ #
469
+ def validates_presence_of(*attr_names)
470
+ configuration = { :message => CouchFoo::Errors.default_error_messages[:blank], :on => :save }
471
+ configuration.update(attr_names.extract_options!)
472
+
473
+ # can't use validates_each here, because it cannot cope with nonexistent attributes,
474
+ # while errors.add_on_empty can
475
+ send(validation_method(configuration[:on]), configuration) do |record|
476
+ record.errors.add_on_blank(attr_names, configuration[:message])
477
+ end
478
+ end
479
+
480
+ # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time:
481
+ #
482
+ # class Person < CouchFoo::Base
483
+ # validates_length_of :first_name, :maximum=>30
484
+ # validates_length_of :last_name, :maximum=>30, :message=>"less than %d if you don't mind"
485
+ # validates_length_of :fax, :in => 7..32, :allow_nil => true
486
+ # validates_length_of :phone, :in => 7..32, :allow_blank => true
487
+ # validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name"
488
+ # validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least %d character"
489
+ # validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with %d characters... don't play me."
490
+ # validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least %d words."), :tokenizer => lambda {|str| str.scan(/\w+/) }
491
+ # end
492
+ #
493
+ # Configuration options:
494
+ # * <tt>:minimum</tt> - The minimum size of the attribute.
495
+ # * <tt>:maximum</tt> - The maximum size of the attribute.
496
+ # * <tt>:is</tt> - The exact size of the attribute.
497
+ # * <tt>:within</tt> - A range specifying the minimum and maximum size of the attribute.
498
+ # * <tt>:in</tt> - A synonym(or alias) for <tt>:within</tt>.
499
+ # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation.
500
+ # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation.
501
+ # * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %d characters)").
502
+ # * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)").
503
+ # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be %d characters)").
504
+ # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message.
505
+ # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
506
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
507
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
508
+ # method, proc or string should return or evaluate to a true or false value.
509
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
510
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
511
+ # method, proc or string should return or evaluate to a true or false value.
512
+ # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to
513
+ # count words as in above example.)
514
+ # Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters.
515
+ def validates_length_of(*attrs)
516
+ # Merge given options with defaults.
517
+ options = {
518
+ :too_long => CouchFoo::Errors.default_error_messages[:too_long],
519
+ :too_short => CouchFoo::Errors.default_error_messages[:too_short],
520
+ :wrong_length => CouchFoo::Errors.default_error_messages[:wrong_length],
521
+ :tokenizer => lambda {|value| value.split(//)}
522
+ }.merge(DEFAULT_VALIDATION_OPTIONS)
523
+ options.update(attrs.extract_options!.symbolize_keys)
524
+
525
+ # Ensure that one and only one range option is specified.
526
+ range_options = ALL_RANGE_OPTIONS & options.keys
527
+ case range_options.size
528
+ when 0
529
+ raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.'
530
+ when 1
531
+ # Valid number of options; do nothing.
532
+ else
533
+ raise ArgumentError, 'Too many range options specified. Choose only one.'
534
+ end
535
+
536
+ # Get range option and value.
537
+ option = range_options.first
538
+ option_value = options[range_options.first]
539
+
540
+ case option
541
+ when :within, :in
542
+ raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)
543
+
544
+ too_short = options[:too_short] % option_value.begin
545
+ too_long = options[:too_long] % option_value.end
546
+
547
+ validates_each(attrs, options) do |record, attr, value|
548
+ value = options[:tokenizer].call(value) if value.kind_of?(String)
549
+ if value.nil? or value.size < option_value.begin
550
+ record.errors.add(attr, too_short)
551
+ elsif value.size > option_value.end
552
+ record.errors.add(attr, too_long)
553
+ end
554
+ end
555
+ when :is, :minimum, :maximum
556
+ raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0
557
+
558
+ # Declare different validations per option.
559
+ validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
560
+ message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }
561
+
562
+ message = (options[:message] || options[message_options[option]]) % option_value
563
+
564
+ validates_each(attrs, options) do |record, attr, value|
565
+ value = options[:tokenizer].call(value) if value.kind_of?(String)
566
+ record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value]
567
+ end
568
+ end
569
+ end
570
+ alias_method :validates_size_of, :validates_length_of
571
+
572
+ # Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user
573
+ # can be named "davidhh".
574
+ #
575
+ # class Person < CouchFoo::Base
576
+ # validates_uniqueness_of :user_name, :scope => :account_id
577
+ # end
578
+ #
579
+ # It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example,
580
+ # making sure that a teacher can only be on the schedule once per semester for a particular class.
581
+ #
582
+ # class TeacherSchedule < CouchFoo::Base
583
+ # validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id]
584
+ # end
585
+ #
586
+ # When the record is created, a check is performed to make sure that no record exists in the database with the given value for the specified
587
+ # attribute (that maps to a property). When the record is updated, the same check is made but disregarding the record itself.
588
+ #
589
+ # Because this check is performed outside the database there is still a chance that duplicate values
590
+ # will be inserted in two parallel transactions. To guarantee against this you should create a
591
+ # unique index on the field. See +add_index+ for more information.
592
+ #
593
+ # Configuration options:
594
+ # * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken").
595
+ # * <tt>:scope</tt> - One or more properties by which to limit the scope of the uniqueness constraint,
596
+ # specified like conditions, eg :scope => {:type => "article"}
597
+ # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
598
+ # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
599
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
600
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
601
+ # method, proc or string should return or evaluate to a true or false value.
602
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
603
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
604
+ # method, proc or string should return or evaluate to a true or false value.
605
+ def validates_uniqueness_of(*attr_names)
606
+ configuration = { :message => CouchFoo::Errors.default_error_messages[:taken], :case_sensitive => true }
607
+ configuration.update(attr_names.extract_options!)
608
+
609
+ validates_each(attr_names,configuration) do |record, attr_name, value|
610
+ # The check for an existing value should be run from a class that
611
+ # isn't abstract. This means working down from the current class
612
+ # (self), to the first non-abstract class. Since classes don't know
613
+ # their subclasses, we have to build the hierarchy between self and
614
+ # the record's class.
615
+ class_hierarchy = [record.class]
616
+ while class_hierarchy.first != self
617
+ class_hierarchy.insert(0, class_hierarchy.first.superclass)
618
+ end
619
+
620
+ # Now we can work our way down the tree to the first non-abstract class
621
+ finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
622
+
623
+ scope = {configuration[:scope] => record.send(configuration[:scope])} if configuration[:scope]
624
+ result = finder_class.find_view(:conditions => {attr_name => value}.merge(scope || {}))
625
+ if !result.empty? && result.first.send(:_id) != record.send(:_id)
626
+ record.errors.add(attr_name, configuration[:message])
627
+ end
628
+ end
629
+ end
630
+
631
+ # Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression
632
+ # provided.
633
+ #
634
+ # class Person < CouchFoo::Base
635
+ # validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create
636
+ # end
637
+ #
638
+ # Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the string, <tt>^</tt> and <tt>$</tt> match the start/end of a line.
639
+ #
640
+ # A regular expression must be provided or else an exception will be raised.
641
+ #
642
+ # Configuration options:
643
+ # * <tt>:message</tt> - A custom error message (default is: "is invalid").
644
+ # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
645
+ # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
646
+ # * <tt>:with</tt> - The regular expression used to validate the format with (note: must be supplied!).
647
+ # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
648
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
649
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
650
+ # method, proc or string should return or evaluate to a true or false value.
651
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
652
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
653
+ # method, proc or string should return or evaluate to a true or false value.
654
+ def validates_format_of(*attr_names)
655
+ configuration = { :message => CouchFoo::Errors.default_error_messages[:invalid], :on => :save, :with => nil }
656
+ configuration.update(attr_names.extract_options!)
657
+
658
+ raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp)
659
+
660
+ validates_each(attr_names, configuration) do |record, attr_name, value|
661
+ record.errors.add(attr_name, configuration[:message] % value) unless value.to_s =~ configuration[:with]
662
+ end
663
+ end
664
+
665
+ # Validates whether the value of the specified attribute is available in a particular enumerable object.
666
+ #
667
+ # class Person < CouchFoo::Base
668
+ # validates_inclusion_of :gender, :in => %w( m f ), :message => "woah! what are you then!??!!"
669
+ # validates_inclusion_of :age, :in => 0..99
670
+ # validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension %s is not included in the list"
671
+ # end
672
+ #
673
+ # Configuration options:
674
+ # * <tt>:in</tt> - An enumerable object of available items.
675
+ # * <tt>:message</tt> - Specifies a custom error message (default is: "is not included in the list").
676
+ # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
677
+ # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
678
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
679
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
680
+ # method, proc or string should return or evaluate to a true or false value.
681
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
682
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
683
+ # method, proc or string should return or evaluate to a true or false value.
684
+ def validates_inclusion_of(*attr_names)
685
+ configuration = { :message => CouchFoo::Errors.default_error_messages[:inclusion], :on => :save }
686
+ configuration.update(attr_names.extract_options!)
687
+
688
+ enum = configuration[:in] || configuration[:within]
689
+
690
+ raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?")
691
+
692
+ validates_each(attr_names, configuration) do |record, attr_name, value|
693
+ record.errors.add(attr_name, configuration[:message] % value) unless enum.include?(value)
694
+ end
695
+ end
696
+
697
+ # Validates that the value of the specified attribute is not in a particular enumerable object.
698
+ #
699
+ # class Person < CouchFoo::Base
700
+ # validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here"
701
+ # validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60"
702
+ # validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension %s is not allowed"
703
+ # end
704
+ #
705
+ # Configuration options:
706
+ # * <tt>:in</tt> - An enumerable object of items that the value shouldn't be part of.
707
+ # * <tt>:message</tt> - Specifies a custom error message (default is: "is reserved").
708
+ # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
709
+ # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
710
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
711
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
712
+ # method, proc or string should return or evaluate to a true or false value.
713
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
714
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
715
+ # method, proc or string should return or evaluate to a true or false value.
716
+ def validates_exclusion_of(*attr_names)
717
+ configuration = { :message => CouchFoo::Errors.default_error_messages[:exclusion], :on => :save }
718
+ configuration.update(attr_names.extract_options!)
719
+
720
+ enum = configuration[:in] || configuration[:within]
721
+
722
+ raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?")
723
+
724
+ validates_each(attr_names, configuration) do |record, attr_name, value|
725
+ record.errors.add(attr_name, configuration[:message] % value) if enum.include?(value)
726
+ end
727
+ end
728
+
729
+ # Validates whether the associated object or objects are all valid themselves. Works with any kind of association.
730
+ #
731
+ # class Book < CouchFoo::Base
732
+ # has_many :pages
733
+ # belongs_to :library
734
+ #
735
+ # validates_associated :pages, :library
736
+ # end
737
+ #
738
+ # Warning: If, after the above definition, you then wrote:
739
+ #
740
+ # class Page < CouchFoo::Base
741
+ # belongs_to :book
742
+ #
743
+ # validates_associated :book
744
+ # end
745
+ #
746
+ # this would specify a circular dependency and cause infinite recursion.
747
+ #
748
+ # NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association
749
+ # is both present and guaranteed to be valid, you also need to use +validates_presence_of+.
750
+ #
751
+ # Configuration options:
752
+ # * <tt>:message</tt> - A custom error message (default is: "is invalid")
753
+ # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
754
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
755
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
756
+ # method, proc or string should return or evaluate to a true or false value.
757
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
758
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
759
+ # method, proc or string should return or evaluate to a true or false value.
760
+ def validates_associated(*attr_names)
761
+ configuration = { :message => CouchFoo::Errors.default_error_messages[:invalid], :on => :save }
762
+ configuration.update(attr_names.extract_options!)
763
+
764
+ validates_each(attr_names, configuration) do |record, attr_name, value|
765
+ record.errors.add(attr_name, configuration[:message]) unless
766
+ (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v }
767
+ end
768
+ end
769
+
770
+ # Validates whether the value of the specified attribute is numeric by trying to convert it to
771
+ # a float with Kernel.Float (if <tt>only_integer</tt> is false) or applying it to the regular expression
772
+ # <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>only_integer</tt> is set to true).
773
+ #
774
+ # class Person < CouchFoo::Base
775
+ # validates_numericality_of :value, :on => :create
776
+ # end
777
+ #
778
+ # Configuration options:
779
+ # * <tt>:message</tt> - A custom error message (default is: "is not a number").
780
+ # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
781
+ # * <tt>:only_integer</tt> - Specifies whether the value has to be an integer, e.g. an integral value (default is +false+).
782
+ # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is +false+). Notice that for fixnum and float properties empty strings are converted to +nil+.
783
+ # * <tt>:greater_than</tt> - Specifies the value must be greater than the supplied value.
784
+ # * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be greater than or equal the supplied value.
785
+ # * <tt>:equal_to</tt> - Specifies the value must be equal to the supplied value.
786
+ # * <tt>:less_than</tt> - Specifies the value must be less than the supplied value.
787
+ # * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less than or equal the supplied value.
788
+ # * <tt>:odd</tt> - Specifies the value must be an odd number.
789
+ # * <tt>:even</tt> - Specifies the value must be an even number.
790
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
791
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
792
+ # method, proc or string should return or evaluate to a true or false value.
793
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
794
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
795
+ # method, proc or string should return or evaluate to a true or false value.
796
+ def validates_numericality_of(*attr_names)
797
+ configuration = { :on => :save, :only_integer => false, :allow_nil => false }
798
+ configuration.update(attr_names.extract_options!)
799
+
800
+
801
+ numericality_options = ALL_NUMERICALITY_CHECKS.keys & configuration.keys
802
+
803
+ (numericality_options - [ :odd, :even ]).each do |option|
804
+ raise ArgumentError, ":#{option} must be a number" unless configuration[option].is_a?(Numeric)
805
+ end
806
+
807
+ validates_each(attr_names,configuration) do |record, attr_name, value|
808
+ raw_value = record.send("#{attr_name}_before_type_cast") || value
809
+
810
+ next if configuration[:allow_nil] and raw_value.nil?
811
+
812
+ if configuration[:only_integer]
813
+ unless raw_value.to_s =~ /\A[+-]?\d+\Z/
814
+ record.errors.add(attr_name, configuration[:message] || CouchFoo::Errors.default_error_messages[:not_a_number])
815
+ next
816
+ end
817
+ raw_value = raw_value.to_i
818
+ else
819
+ begin
820
+ raw_value = Kernel.Float(raw_value)
821
+ rescue ArgumentError, TypeError
822
+ record.errors.add(attr_name, configuration[:message] || CouchFoo::Errors.default_error_messages[:not_a_number])
823
+ next
824
+ end
825
+ end
826
+
827
+ numericality_options.each do |option|
828
+ case option
829
+ when :odd, :even
830
+ record.errors.add(attr_name, configuration[:message] || CouchFoo::Errors.default_error_messages[option]) unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
831
+ else
832
+ message = configuration[:message] || CouchFoo::Errors.default_error_messages[option]
833
+ message = message % configuration[option] if configuration[option]
834
+ record.errors.add(attr_name, message) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
835
+ end
836
+ end
837
+ end
838
+ end
839
+
840
+ # Creates an object just like Base.create but calls save! instead of save
841
+ # so an exception is raised if the record is invalid.
842
+ def create!(attributes = nil, &block)
843
+ if attributes.is_a?(Array)
844
+ attributes.collect { |attr| create!(attr, &block) }
845
+ else
846
+ object = new(attributes)
847
+ yield(object) if block_given?
848
+ object.save!
849
+ object
850
+ end
851
+ end
852
+
853
+ private
854
+ def validation_method(on)
855
+ case on
856
+ when :save then :validate
857
+ when :create then :validate_on_create
858
+ when :update then :validate_on_update
859
+ end
860
+ end
861
+ end
862
+
863
+ # The validation process on save can be skipped by passing false. The regular Base#save method is
864
+ # replaced with this when the validations module is mixed in, which it is by default.
865
+ def save_with_validation(perform_validation = true)
866
+ if perform_validation && valid? || !perform_validation
867
+ save_without_validation()
868
+ else
869
+ false
870
+ end
871
+ end
872
+
873
+ # Attempts to save the record just like Base#save but will raise a RecordInvalid exception instead of returning false
874
+ # if the record is not valid.
875
+ def save_with_validation!()
876
+ if valid?
877
+ save_without_validation!()
878
+ else
879
+ raise RecordInvalid.new(self)
880
+ end
881
+ end
882
+
883
+ # Updates a single attribute and saves the record without going through the normal validation procedure.
884
+ # This is especially useful for boolean flags on existing records. The regular +update_attribute+ method
885
+ # in Base is replaced with this when the validations module is mixed in, which it is by default.
886
+ def update_attribute_with_validation_skipping(name, value)
887
+ send(name.to_s + '=', value)
888
+ save(false)
889
+ end
890
+
891
+ # Runs +validate+ and +validate_on_create+ or +validate_on_update+ and returns true if no errors were added otherwise false.
892
+ def valid?
893
+ errors.clear
894
+
895
+ run_callbacks(:validate)
896
+ validate
897
+
898
+ if new_record?
899
+ run_callbacks(:validate_on_create)
900
+ validate_on_create
901
+ else
902
+ run_callbacks(:validate_on_update)
903
+ validate_on_update
904
+ end
905
+
906
+ errors.empty?
907
+ end
908
+
909
+ # Returns the Errors object that holds all information about attribute error messages.
910
+ def errors
911
+ @errors ||= Errors.new(self)
912
+ end
913
+
914
+ protected
915
+ # Overwrite this method for validation checks on all saves and use <tt>Errors.add(field, msg)</tt> for invalid attributes.
916
+ def validate #:doc:
917
+ end
918
+
919
+ # Overwrite this method for validation checks used only on creation.
920
+ def validate_on_create #:doc:
921
+ end
922
+
923
+ # Overwrite this method for validation checks used only on updates.
924
+ def validate_on_update # :doc:
925
+ end
926
+ end
927
+ end