Commit 5ac3a1acf1bcf9601d9aaf9048cae009e3321941

moved dsl files under document/dsl directory

Commit diff

lib/strokedb/document.rb

 
1require 'document/validations'
2require 'document/virtualize'
1require 'document/dsl'
32require 'document/util'
43require 'document/meta'
5require 'document/associations'
64require 'document/callback'
7require 'document/coercions'
85require 'document/delete'
96require 'document/slot'
107require 'document/versions'
toggle raw diff

lib/strokedb/document/associations.rb

 
0module StrokeDB
1
2 module Associations
3
4 module HasManyAssociation
5 attr_reader :association_owner, :association_slotname
6 def new(slots={})
7 association_meta.constantize.new(association_owner.store, slots.merge({association_reference_slotname => association_owner}))
8 end
9 alias :build :new
10
11 def create!(slots={})
12 new(slots).save!
13 end
14
15 def find(query={})
16 association_owner._has_many_association(association_slotname,query)
17 end
18 def <<(doc)
19 doc.update_slots! association_reference_slotname => association_owner
20 self
21 end
22
23 private
24
25 def association_reference_slotname
26 association_owner.meta["has_many_#{association_slotname}"][:reference_slotname]
27 end
28
29 def association_meta
30 association_owner.meta["has_many_#{association_slotname}"][:meta]
31 end
32
33 end
34
35 def has_many(slotname, opts={}, &block)
36 opts = opts.stringify_keys
37
38 reference_slotname = opts['foreign_reference']
39 through = opts['through'] || []
40 through = [through] unless through.is_a?(Array)
41 meta = (through.shift || slotname).to_s.singularize.camelize
42 query = opts['conditions'] || {}
43
44 extend_with = opts['extend'] || block
45
46 @meta_initialization_procs << Proc.new do
47 case extend_with
48 when Proc
49 extend_with_proc = extend_with
50 extend_with = "HasMany#{slotname.to_s.camelize}"
51 const_set(extend_with, Module.new(&extend_with_proc))
52 extend_with = "#{self.name}::HasMany#{slotname.to_s.camelize}"
53 when Module
54 extend_with = extend_with.name
55 when NilClass
56 else
57 raise "has_many extension should be either Module or Proc"
58 end
59 reference_slotname = reference_slotname || name.demodulize.tableize.singularize
60 if name.index('::') # we're in namespaced meta
61 _t = name.split('::')
62 _t.pop
63 _t << meta
64 meta = _t.join('::')
65 end
66 @args.last.reverse_merge!({"has_many_#{slotname}" => { :reference_slotname => reference_slotname, :through => through, :meta => meta, :query => query, :extend_with => extend_with } })
67 define_method(slotname) do
68 _has_many_association(slotname,{})
69 end
70
71 end
72
73 end
74
75 private
76
77 def initialize_associations
78 define_method(:_has_many_association) do |slotname, additional_query|
79 slot_has_many = meta["has_many_#{slotname}"]
80 reference_slotname = slot_has_many[:reference_slotname]
81 through = slot_has_many[:through]
82 meta = slot_has_many[:meta]
83 query = slot_has_many[:query]
84 effective_query = query.merge(:meta => meta.constantize.document, reference_slotname => self).merge(additional_query)
85
86 result = LazyArray.new.load_with do |lazy_array|
87 store.search(effective_query).map do |d|
88 begin
89 through.each { |t| d = d.send(t) }
90 rescue SlotNotFoundError
91 d = nil
92 end
93 d
94 end.compact
95 end
96 if extend_with = slot_has_many[:extend_with]
97 result.extend(extend_with.constantize)
98 end
99 result.instance_variable_set(:@association_owner, self)
100 result.instance_variable_set(:@association_slotname, slotname)
101 result.extend(HasManyAssociation)
102 result
103 end
104 end
105 end
106end
toggle raw diff

lib/strokedb/document/coercions.rb

 
0module StrokeDB
1 module Coercions
2 def coerces(slotnames, opts = {})
3 opts = opts.stringify_keys
4 raise ArgumentError, "coerces should have :to specified" unless opts['to']
5
6 check_condition(opts['if']) if opts['if']
7 check_condition(opts['unless']) if opts['unless']
8
9 slotnames = [slotnames] unless slotnames.is_a?(Array)
10 slotnames.each {|slotname| register_coercion(slotname, opts)}
11 end
12
13 private
14
15 def initialize_coercions
16 on_set_slot(:coerces) do |doc, slotname, value|
17 if coercion = doc.meta["coerces_#{slotname}"]
18 should_call = (!coercion[:if] || evaluate_condition(coercion[:if], doc)) &&
19 (!coercion[:unless] || !evaluate_condition(coercion[:unless], doc))
20 if should_call
21 case coercion[:to]
22 when 'number'
23 if value.to_i.to_s == value
24 value.to_i
25 else
26 value
27 end
28 when 'string'
29 value.to_s
30 end
31 end
32 end
33 end
34 end
35
36 def register_coercion(slotname, opts)
37 slotname = slotname.to_s
38 to = opts['to'].to_s
39
40 options_hash = {
41 :slotname => slotname,
42 :if => opts['if'],
43 :unless => opts['unless'],
44 :to => to
45 }
46
47 # options_hash.merge!(yield(opts)) if block_given?
48
49 coercion_slot = "coerces_#{slotname}"
50
51 @meta_initialization_procs << Proc.new do
52 @args.last.reverse_merge!(coercion_slot => { :meta => name }.merge(options_hash))
53 end
54 end
55 end
56end
toggle raw diff

lib/strokedb/document/dsl.rb

 
1require 'document/dsl/validations'
2require 'document/dsl/virtualize'
3require 'document/dsl/associations'
4require 'document/dsl/coercions'
toggle raw diff

lib/strokedb/document/dsl/associations.rb

 
1module StrokeDB
2
3 module Associations
4
5 module HasManyAssociation
6 attr_reader :association_owner, :association_slotname
7 def new(slots={})
8 association_meta.constantize.new(association_owner.store, slots.merge({association_reference_slotname => association_owner}))
9 end
10 alias :build :new
11
12 def create!(slots={})
13 new(slots).save!
14 end
15
16 def find(query={})
17 association_owner._has_many_association(association_slotname,query)
18 end
19 def <<(doc)
20 doc.update_slots! association_reference_slotname => association_owner
21 self
22 end
23
24 private
25
26 def association_reference_slotname
27 association_owner.meta["has_many_#{association_slotname}"][:reference_slotname]
28 end
29
30 def association_meta
31 association_owner.meta["has_many_#{association_slotname}"][:meta]
32 end
33
34 end
35
36 def has_many(slotname, opts={}, &block)
37 opts = opts.stringify_keys
38
39 reference_slotname = opts['foreign_reference']
40 through = opts['through'] || []
41 through = [through] unless through.is_a?(Array)
42 meta = (through.shift || slotname).to_s.singularize.camelize
43 query = opts['conditions'] || {}
44
45 extend_with = opts['extend'] || block
46
47 @meta_initialization_procs << Proc.new do
48 case extend_with
49 when Proc
50 extend_with_proc = extend_with
51 extend_with = "HasMany#{slotname.to_s.camelize}"
52 const_set(extend_with, Module.new(&extend_with_proc))
53 extend_with = "#{self.name}::HasMany#{slotname.to_s.camelize}"
54 when Module
55 extend_with = extend_with.name
56 when NilClass
57 else
58 raise "has_many extension should be either Module or Proc"
59 end
60 reference_slotname = reference_slotname || name.demodulize.tableize.singularize
61 if name.index('::') # we're in namespaced meta
62 _t = name.split('::')
63 _t.pop
64 _t << meta
65 meta = _t.join('::')
66 end
67 @args.last.reverse_merge!({"has_many_#{slotname}" => { :reference_slotname => reference_slotname, :through => through, :meta => meta, :query => query, :extend_with => extend_with } })
68 define_method(slotname) do
69 _has_many_association(slotname,{})
70 end
71
72 end
73
74 end
75
76 private
77
78 def initialize_associations
79 define_method(:_has_many_association) do |slotname, additional_query|
80 slot_has_many = meta["has_many_#{slotname}"]
81 reference_slotname = slot_has_many[:reference_slotname]
82 through = slot_has_many[:through]
83 meta = slot_has_many[:meta]
84 query = slot_has_many[:query]
85 effective_query = query.merge(:meta => meta.constantize.document, reference_slotname => self).merge(additional_query)
86
87 result = LazyArray.new.load_with do |lazy_array|
88 store.search(effective_query).map do |d|
89 begin
90 through.each { |t| d = d.send(t) }
91 rescue SlotNotFoundError
92 d = nil
93 end
94 d
95 end.compact
96 end
97 if extend_with = slot_has_many[:extend_with]
98 result.extend(extend_with.constantize)
99 end
100 result.instance_variable_set(:@association_owner, self)
101 result.instance_variable_set(:@association_slotname, slotname)
102 result.extend(HasManyAssociation)
103 result
104 end
105 end
106 end
107end
toggle raw diff

lib/strokedb/document/dsl/coercions.rb

 
1module StrokeDB
2 module Coercions
3 def coerces(slotnames, opts = {})
4 opts = opts.stringify_keys
5 raise ArgumentError, "coerces should have :to specified" unless opts['to']
6
7 check_condition(opts['if']) if opts['if']
8 check_condition(opts['unless']) if opts['unless']
9
10 slotnames = [slotnames] unless slotnames.is_a?(Array)
11 slotnames.each {|slotname| register_coercion(slotname, opts)}
12 end
13
14 private
15
16 def initialize_coercions
17 on_set_slot(:coerces) do |doc, slotname, value|
18 if coercion = doc.meta["coerces_#{slotname}"]
19 should_call = (!coercion[:if] || evaluate_condition(coercion[:if], doc)) &&
20 (!coercion[:unless] || !evaluate_condition(coercion[:unless], doc))
21 if should_call
22 case coercion[:to]
23 when 'number'
24 if value.to_i.to_s == value
25 value.to_i
26 else
27 value
28 end
29 when 'string'
30 value.to_s
31 end
32 end
33 end
34 end
35 end
36
37 def register_coercion(slotname, opts)
38 slotname = slotname.to_s
39 to = opts['to'].to_s
40
41 options_hash = {
42 :slotname => slotname,
43 :if => opts['if'],
44 :unless => opts['unless'],
45 :to => to
46 }
47
48 # options_hash.merge!(yield(opts)) if block_given?
49
50 coercion_slot = "coerces_#{slotname}"
51
52 @meta_initialization_procs << Proc.new do
53 @args.last.reverse_merge!(coercion_slot => { :meta => name }.merge(options_hash))
54 end
55 end
56 end
57end
toggle raw diff

lib/strokedb/document/dsl/meta_dsl.rb

 
1module StrokeDB
2 module MetaDSL
3 def on_initialize(&block)
4 store_dsl_options("on_initialize", block)
5 end
6 end
7end
toggle raw diff

lib/strokedb/document/dsl/validations.rb

 
1require 'ostruct'
2
3module StrokeDB
4 module Validations
5 ERROR_MESSAGES = {
6 :should_be_present => '#{meta}\'s #{slotname} should be present on #{on}',
7 :invalid_type => '#{meta}\'s #{slotname} should be of type #{validation_type}',
8 :already_exists => 'A document with a #{slotname} of #{slotvalue} already exists',
9 :not_included => 'Value of #{slotname} is not included in the list',
10 :not_excluded => 'Value of #{slotname} is reserved',
11 :invalid_format => 'Value of #{slotname} should match #{slotvalue}',
12 :not_confirmed => '#{meta}\'s #{slotname} doesn\'t match confirmation',
13 :not_accepted => '#{slotname} must be accepted',
14 :wrong_length => '#{slotname} has the wrong length (should be %d characters)',
15 :too_short => '#{slotname} is too short (minimum is %d characters)',
16 :too_long => '#{slotname} is too long (maximum is %d characters)',
17 :invalid => '#{slotname} is invalid',
18 :must_be_integer => '#{slotname} must be integer',
19 :not_a_number => '#{slotname} is not a number',
20 }.freeze unless defined? ERROR_MESSAGES
21
22 # Validates that the specified slot exists in the document. Happens by default on save. Example:
23 #
24 # Person = Meta.new do
25 # validates_presence_of :first_name
26 # end
27 #
28 # The first_name slot must be in the document.
29 #
30 # Configuration options:
31 # * <tt>message</tt> - A custom error message (default is: "should be present on ...")
32 # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
33 # * <tt>if</tt> - Specifies a method or slot name to call to determine if the validation should
34 # occur (e.g. :if => :allow_validation, or :if => 'signup_step_less_than_three'). The
35 # method result or slot should be equal to a true or false value.
36 # * <tt>unless</tt> - Specifies a method or slot name to call to determine if the validation should
37 # not occur (e.g. :unless => :skip_validation, or :unless => 'signup_step_less_than_three'). The
38 # method result or slot should be equal to a true or false value.
39 def validates_presence_of(slotname, opts={})
40 register_validation("presence_of", slotname, opts, :should_be_present)
41 end
42
43 # Validates that the specified slot value has a specific type. Happens by default on save. Example:
44 #
45 # Person = Meta.new do
46 # validates_type_of :first_name, :as => :string
47 # end
48 #
49 # The first_name value for each Person must be unique.
50 #
51 # Configuration options:
52 # * <tt>message</tt> - A custom error message (default is: "document with value already exists")
53 # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
54 # * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false)
55 # * <tt>if</tt> - Specifies a method or slot name to call to determine if the validation should
56 # occur (e.g. :if => :allow_validation, or :if => 'signup_step_less_than_three'). The
57 # method result or slot should be equal to a true or false value.
58 # * <tt>unless</tt> - Specifies a method or slot name to call to determine if the validation should
59 # not occur (e.g. :unless => :skip_validation, or :unless => 'signup_step_less_than_three'). The
60 # method result or slot should be equal to a true or false value.
61 #
62 # === Warning
63 # When the slot doesn't exist, validation gets skipped.
64 def validates_type_of(slotname, opts={})
65 register_validation("type_of", slotname, opts, :invalid_type) do |opts|
66 raise ArgumentError, "validates_type_of requires :as => type" unless type = opts['as']
67
68 {
69 :validation_type => type.to_s.camelize,
70 :allow_nil => !!opts['allow_nil']
71 }
72 end
73 end
74
75 # Validates that the specified slot value is unique in the store
76 #
77 # Person = Meta.new do
78 # validates_uniqueness_of :first_name
79 # end
80 #
81 # The first_name slot must be in the document.
82 #
83 # Configuration options:
84 # * <tt>message</tt> - A custom error message (default is: "A document with a ... of ... already exists")
85 # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
86 # * <tt>case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (true by default). NOT YET IMPLEMENTED
87 # * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false)
88 # * <tt>allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is: false)
89 # * <tt>if</tt> - Specifies a method or slot name to call to determine if the validation should
90 # occur (e.g. :if => :allow_validation, or :if => 'signup_step_less_than_three'). The
91 # method result or slot should be equal to a true or false value.
92 # * <tt>unless</tt> - Specifies a method or slot name to call to determine if the validation should
93 # not occur (e.g. :unless => :skip_validation, or :unless => 'signup_step_less_than_three'). The
94 # method result or slot should be equal to a true or false value.
95 def validates_uniqueness_of(slotname, opts={})
96 register_validation("uniqueness_of", slotname, opts, :already_exists) do |opts|
97 { :allow_nil => !!opts['allow_nil'], :allow_blank => !!opts['allow_blank'] }
98 end
99 end
100
101 # Validates whether the value of the specified slot is available in a particular enumerable object.
102 #
103 # Person = Meta.new do
104 # validates_inclusion_of :gender, :in => %w( m f ), :message => "woah! what are you then!??!!"
105 # validates_inclusion_of :age, :in => 0..99
106 # validates_inclusion_of :format, :in => %w( jpg gif png ), :message => 'extension #{slotvalue} is not included in the list'
107 # end
108 #
109 # Configuration options:
110 # * <tt>in</tt> - An enumerable object of available items
111 # * <tt>message</tt> - Specifies a customer error message (default is: "is
112 # not included in the list")
113 # * <tt>allow_nil</tt> - If set to true, skips this validation if the slot is null (default is: false)
114 # * <tt>allow_blank</tt> - If set to true, skips this validation if the slot is blank (default is: false)
115 # * <tt>if</tt> - Specifies a method or slot name to call to determine if the validation should
116 # occur (e.g. :if => :allow_validation, or :if => 'signup_step_less_than_three'). The
117 # method result or slot should be equal to a true or false value.
118 # * <tt>unless</tt> - Specifies a method or slot name to call to determine if the validation should
119 # not occur (e.g. :unless => :skip_validation, or :unless => 'signup_step_less_than_three'). The
120 # method result or slot should be equal to a true or false value.
121 def validates_inclusion_of(slotname, opts={})
122 register_validation("inclusion_of", slotname, opts, :not_included) do |opts|
123 raise ArgumentError, "validates_inclusion_of requires :in set" unless opts['in']
124 raise ArgumentError, "object must respond to the method include?" unless opts['in'].respond_to? :include?
125
126 {
127 :in => opts['in'],
128 :allow_nil => !!opts['allow_nil'],
129 :allow_blank => !!opts['allow_blank']
130 }
131 end
132 end
133
134 # Validates that the value of the specified slot is not in a particular enumerable object.
135 #
136 # Person = Meta.new do
137 # validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here"
138 # validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60"
139 # validates_exclusion_of :format, :in => %w( mov avi ), :message => 'extension #{slotvalue} is not allowed'
140 # end
141 #
142 # Configuration options:
143 # * <tt>in</tt> - An enumerable object of items that the value shouldn't be part of
144 # * <tt>message</tt> - Specifies a customer error message (default is: "is reserved")
145 # * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false)
146 # * <tt>allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is: false)
147 # * <tt>if</tt> - Specifies a method or slot name to call to determine if the validation should
148 # occur (e.g. :if => :allow_validation, or :if => 'signup_step_less_than_three'). The
149 # method result or slot should be equal to a true or false value.
150 # * <tt>unless</tt> - Specifies a method or slot name to call to determine if the validation should
151 # not occur (e.g. :unless => :skip_validation, or :unless => 'signup_step_less_than_three'). The
152 # method result or slot should be equal to a true or false value.
153 def validates_exclusion_of(slotname, opts={})
154 register_validation("exclusion_of", slotname, opts, :not_excluded) do |opts|
155 raise ArgumentError, "validates_exclusion_of requires :in set" unless opts['in']
156 raise ArgumentError, "object must respond to the method include?" unless opts['in'].respond_to? :include?
157
158 {
159 :in => opts['in'],
160 :allow_nil => !!opts['allow_nil'],
161 :allow_blank => !!opts['allow_blank']
162 }
163 end
164 end
165
166 # Validates whether the value of the specified attribute is numeric by trying to convert it to
167 # a float with Kernel.Float (if <tt>only_integer</tt> is false) or applying it to the regular expression
168 # <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>only_integer</tt> is set to true).
169 #
170 # Item = Meta.new do
171 # validates_numericality_of :price
172 # end
173 #
174 # * <tt>message</tt> - A custom error message (default is: "is not a number")
175 # * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
176 # * <tt>only_integer</tt> Specifies whether the value has to be an integer, e.g. an integral value (default is false)
177 # * <tt>allow_nil</tt> Skip validation if attribute is nil (default is
178 # false). Notice that for fixnum and float columns empty strings are converted to nil
179 # * <tt>greater_than</tt> Specifies the value must be greater than the supplied value
180 # * <tt>greater_than_or_equal_to</tt> Specifies the value must be greater than or equal the supplied value
181 # * <tt>equal_to</tt> Specifies the value must be equal to the supplied value
182 # * <tt>less_than</tt> Specifies the value must be less than the supplied value
183 # * <tt>less_than_or_equal_to</tt> Specifies the value must be less than or equal the supplied value
184 # * <tt>odd</tt> Specifies the value must be an odd number
185 # * <tt>even</tt> Specifies the value must be an even number
186 # * <tt>if</tt> - Specifies a method or slot name to call to determine if the validation should
187 # occur (e.g. :if => :allow_validation, or :if => 'signup_step_less_than_three'). The
188 # method result or slot should be equal to a true or false value.
189 # * <tt>unless</tt> - Specifies a method or slot name to call to determine if the validation should
190 # not occur (e.g. :unless => :skip_validation, or :unless => 'signup_step_less_than_three'). The
191 # method result or slot should be equal to a true or false value.
192 NUMERICALITY_CHECKS = { 'greater_than' => :>, 'greater_than_or_equal_to' => :>=,
193 'equal_to' => :==, 'less_than' => :<, 'less_than_or_equal_to' => :<=,
194 'odd' => :odd?, 'even' => :even? }.freeze
195
196 def validates_numericality_of(slotname, opts={})
197 register_validation("numericality_of", slotname, opts, nil) do |opts|
198 numeric_checks = opts.reject { |key, val| !NUMERICALITY_CHECKS.include? key }
199
200 %w(odd even).each do |o|
201 raise ArgumentError, ":#{o} must be set to true if set at all" if opts.include?(o) && opts[o] != true
202 end
203
204 (numeric_checks.keys - %w(odd even)).each do |option|
205 raise ArgumentError, "#{option} must be a number" unless opts[option].is_a? Numeric
206 end
207
208 {
209 :only_integer => opts['only_integer'],
210 :numeric_checks => numeric_checks,
211 :allow_nil => !!opts['allow_nil']
212 }
213 end
214 end
215
216 # Validates whether the value of the specified attribute is of the correct
217 # form by matching it against the regular expression provided.
218 #
219 # Person = Meta.new do
220 # validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create
221 # end
222 #
223 # Note: use \A and \Z to match the start and end of the string, ^ and $ match the start/end of a line.
224 #
225 # A regular expression must be provided or else an exception will be
226 # raised.
227 #
228 # Configuration options:
229 # * <tt>message</tt> - A custom error message (default is: "is invalid")
230 # * <tt>with</tt> - The regular expression used to validate the format with (note: must be supplied!)
231 # * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
232 # * <tt>if</tt> - Specifies a method or slot name to call to determine if the validation should
233 # occur (e.g. :if => :allow_validation, or :if => 'signup_step_less_than_three'). The
234 # method result or slot should be equal to a true or false value.
235 # * <tt>unless</tt> - Specifies a method or slot name to call to determine if the validation should
236 # not occur (e.g. :unless => :skip_validation, or :unless => 'signup_step_less_than_three'). The
237 # method result or slot should be equal to a true or false value.
238 def validates_format_of(slotname, opts={})
239 register_validation("format_of", slotname, opts, :invalid_format) do |opts|
240 unless regexp = opts['with'].is_a?(Regexp)
241 raise ArgumentError, "validates_format_of requires :with => regexp"
242 end
243 { :with => opts['with'] }
244 end
245 end
246
247 # Encapsulates the pattern of wanting to validate a password or email
248 # address field with a confirmation. Example:
249 #
250 # Model:
251 # Person = Meta.new do
252 # validates_confirmation_of :password
253 # validates_confirmation_of :email_address, :message => "should match confirmation"
254 # end
255 #
256 # View:
257 # <%= password_field "person", "password" %>
258 # <%= password_field "person", "password_confirmation" %>
259 #
260 # The added +password_confirmation+ slot is virtual; it exists only as
261 # an in-memory slot for validating the password. To achieve this, the
262 # validation adds accessors to the model for the confirmation slot.
263 # NOTE: This check is performed only if +password_confirmation+ is not nil,
264 # and by default only on save. To require confirmation, make sure to add a
265 # presence check for the confirmation attribute:
266 #
267 # validates_presence_of :password_confirmation, :if => :password_changed?
268 #
269 # Configuration options:
270 # * <tt>message</tt> - A custom error message (default is: "doesn't match confirmation")
271 # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
272 # * <tt>if</tt> - Specifies a method or slot name to call to determine if the validation should
273 # occur (e.g. :if => :allow_validation, or :if => 'signup_step_less_than_three'). The
274 # method result or slot should be equal to a true or false value.
275 # * <tt>unless</tt> - Specifies a method or slot name to call to determine if the validation should
276 # not occur (e.g. :unless => :skip_validation, or :unless => 'signup_step_less_than_three'). The
277 # method result or slot should be equal to a true or false value.
278 def validates_confirmation_of(slotname, opts = {})
279 register_validation("confirmation_of", slotname, opts, :not_confirmed)
280
281 virtualizes(slotname.to_s + "_confirmation")
282 end
283
284 # Encapsulates the pattern of wanting to validate the acceptance of a terms
285 # of service check box (or similar agreement). Example:
286 #
287 # Person = Meta.new do