strokedb 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/CONTRIBUTORS +7 -0
  2. data/CREDITS +13 -0
  3. data/README +44 -0
  4. data/bin/sdbc +2 -0
  5. data/lib/config/config.rb +161 -0
  6. data/lib/data_structures/inverted_list.rb +297 -0
  7. data/lib/data_structures/point_query.rb +24 -0
  8. data/lib/data_structures/skiplist.rb +302 -0
  9. data/lib/document/associations.rb +107 -0
  10. data/lib/document/callback.rb +11 -0
  11. data/lib/document/coercions.rb +57 -0
  12. data/lib/document/delete.rb +28 -0
  13. data/lib/document/document.rb +684 -0
  14. data/lib/document/meta.rb +261 -0
  15. data/lib/document/slot.rb +199 -0
  16. data/lib/document/util.rb +27 -0
  17. data/lib/document/validations.rb +704 -0
  18. data/lib/document/versions.rb +106 -0
  19. data/lib/document/virtualize.rb +82 -0
  20. data/lib/init.rb +57 -0
  21. data/lib/stores/chainable_storage.rb +57 -0
  22. data/lib/stores/inverted_list_index/inverted_list_file_storage.rb +56 -0
  23. data/lib/stores/inverted_list_index/inverted_list_index.rb +49 -0
  24. data/lib/stores/remote_store.rb +172 -0
  25. data/lib/stores/skiplist_store/chunk.rb +119 -0
  26. data/lib/stores/skiplist_store/chunk_storage.rb +21 -0
  27. data/lib/stores/skiplist_store/file_chunk_storage.rb +44 -0
  28. data/lib/stores/skiplist_store/memory_chunk_storage.rb +37 -0
  29. data/lib/stores/skiplist_store/skiplist_store.rb +217 -0
  30. data/lib/stores/store.rb +5 -0
  31. data/lib/sync/chain_sync.rb +38 -0
  32. data/lib/sync/diff.rb +126 -0
  33. data/lib/sync/lamport_timestamp.rb +81 -0
  34. data/lib/sync/store_sync.rb +79 -0
  35. data/lib/sync/stroke_diff/array.rb +102 -0
  36. data/lib/sync/stroke_diff/default.rb +21 -0
  37. data/lib/sync/stroke_diff/hash.rb +186 -0
  38. data/lib/sync/stroke_diff/string.rb +116 -0
  39. data/lib/sync/stroke_diff/stroke_diff.rb +9 -0
  40. data/lib/util/blankslate.rb +42 -0
  41. data/lib/util/ext/blank.rb +50 -0
  42. data/lib/util/ext/enumerable.rb +36 -0
  43. data/lib/util/ext/fixnum.rb +16 -0
  44. data/lib/util/ext/hash.rb +22 -0
  45. data/lib/util/ext/object.rb +8 -0
  46. data/lib/util/ext/string.rb +35 -0
  47. data/lib/util/inflect.rb +217 -0
  48. data/lib/util/java_util.rb +9 -0
  49. data/lib/util/lazy_array.rb +54 -0
  50. data/lib/util/lazy_mapping_array.rb +64 -0
  51. data/lib/util/lazy_mapping_hash.rb +46 -0
  52. data/lib/util/serialization.rb +29 -0
  53. data/lib/util/trigger_partition.rb +136 -0
  54. data/lib/util/util.rb +38 -0
  55. data/lib/util/xml.rb +6 -0
  56. data/lib/view/view.rb +55 -0
  57. data/script/console +70 -0
  58. data/strokedb.rb +75 -0
  59. metadata +148 -0
@@ -0,0 +1,704 @@
1
+ require 'ostruct'
2
+
3
+ module 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
+ }.freeze unless defined? ERROR_MESSAGES
19
+
20
+ # Validates that the specified slot exists in the document. Happens by default on save. Example:
21
+ #
22
+ # Person = Meta.new do
23
+ # validates_presence_of :first_name
24
+ # end
25
+ #
26
+ # The first_name slot must be in the document.
27
+ #
28
+ # Configuration options:
29
+ # * <tt>message</tt> - A custom error message (default is: "should be present on ...")
30
+ # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
31
+ # * <tt>if</tt> - Specifies a method or slot name to call to determine if the validation should
32
+ # occur (e.g. :if => :allow_validation, or :if => 'signup_step_less_than_three'). The
33
+ # method result or slot should be equal to a true or false value.
34
+ # * <tt>unless</tt> - Specifies a method or slot name to call to determine if the validation should
35
+ # not occur (e.g. :unless => :skip_validation, or :unless => 'signup_step_less_than_three'). The
36
+ # method result or slot should be equal to a true or false value.
37
+ def validates_presence_of(slotname, opts={})
38
+ register_validation("presence_of", slotname, opts, :should_be_present)
39
+ end
40
+
41
+ # Validates that the specified slot value has a specific type. Happens by default on save. Example:
42
+ #
43
+ # Person = Meta.new do
44
+ # validates_type_of :first_name, :as => :string
45
+ # end
46
+ #
47
+ # The first_name value for each Person must be unique.
48
+ #
49
+ # Configuration options:
50
+ # * <tt>message</tt> - A custom error message (default is: "document with value already exists")
51
+ # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
52
+ # * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false)
53
+ # * <tt>if</tt> - Specifies a method or slot name to call to determine if the validation should
54
+ # occur (e.g. :if => :allow_validation, or :if => 'signup_step_less_than_three'). The
55
+ # method result or slot should be equal to a true or false value.
56
+ # * <tt>unless</tt> - Specifies a method or slot name to call to determine if the validation should
57
+ # not occur (e.g. :unless => :skip_validation, or :unless => 'signup_step_less_than_three'). The
58
+ # method result or slot should be equal to a true or false value.
59
+ #
60
+ # === Warning
61
+ # When the slot doesn't exist, validation gets skipped.
62
+ def validates_type_of(slotname, opts={})
63
+ register_validation("type_of", slotname, opts, :invalid_type) do |opts|
64
+ raise ArgumentError, "validates_type_of requires :as => type" unless type = opts['as']
65
+
66
+ {
67
+ :validation_type => type.to_s.camelize,
68
+ :allow_nil => !!opts['allow_nil']
69
+ }
70
+ end
71
+ end
72
+
73
+ # Validates that the specified slot value is unique in the store
74
+ #
75
+ # Person = Meta.new do
76
+ # validates_uniqueness_of :first_name
77
+ # end
78
+ #
79
+ # The first_name slot must be in the document.
80
+ #
81
+ # Configuration options:
82
+ # * <tt>message</tt> - A custom error message (default is: "A document with a ... of ... already exists")
83
+ # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
84
+ # * <tt>case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (true by default). NOT YET IMPLEMENTED
85
+ # * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false)
86
+ # * <tt>allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is: false)
87
+ # * <tt>if</tt> - Specifies a method or slot name to call to determine if the validation should
88
+ # occur (e.g. :if => :allow_validation, or :if => 'signup_step_less_than_three'). The
89
+ # method result or slot should be equal to a true or false value.
90
+ # * <tt>unless</tt> - Specifies a method or slot name to call to determine if the validation should
91
+ # not occur (e.g. :unless => :skip_validation, or :unless => 'signup_step_less_than_three'). The
92
+ # method result or slot should be equal to a true or false value.
93
+ def validates_uniqueness_of(slotname, opts={})
94
+ register_validation("uniqueness_of", slotname, opts, :already_exists) do |opts|
95
+ { :allow_nil => !!opts['allow_nil'], :allow_blank => !!opts['allow_blank'] }
96
+ end
97
+ end
98
+
99
+ # Validates whether the value of the specified slot is available in a particular enumerable object.
100
+ #
101
+ # Person = Meta.new do
102
+ # validates_inclusion_of :gender, :in => %w( m f ), :message => "woah! what are you then!??!!"
103
+ # validates_inclusion_of :age, :in => 0..99
104
+ # validates_inclusion_of :format, :in => %w( jpg gif png ), :message => 'extension #{slotvalue} is not included in the list'
105
+ # end
106
+ #
107
+ # Configuration options:
108
+ # * <tt>in</tt> - An enumerable object of available items
109
+ # * <tt>message</tt> - Specifies a customer error message (default is: "is
110
+ # not included in the list")
111
+ # * <tt>allow_nil</tt> - If set to true, skips this validation if the slot is null (default is: false)
112
+ # * <tt>allow_blank</tt> - If set to true, skips this validation if the slot is blank (default is: false)
113
+ # * <tt>if</tt> - Specifies a method or slot name to call to determine if the validation should
114
+ # occur (e.g. :if => :allow_validation, or :if => 'signup_step_less_than_three'). The
115
+ # method result or slot should be equal to a true or false value.
116
+ # * <tt>unless</tt> - Specifies a method or slot name to call to determine if the validation should
117
+ # not occur (e.g. :unless => :skip_validation, or :unless => 'signup_step_less_than_three'). The
118
+ # method result or slot should be equal to a true or false value.
119
+ def validates_inclusion_of(slotname, opts={})
120
+ register_validation("inclusion_of", slotname, opts, :not_included) do |opts|
121
+ raise ArgumentError, "validates_inclusion_of requires :in set" unless opts['in']
122
+ raise ArgumentError, "object must respond to the method include?" unless opts['in'].respond_to? :include?
123
+
124
+ {
125
+ :in => opts['in'],
126
+ :allow_nil => !!opts['allow_nil'],
127
+ :allow_blank => !!opts['allow_blank']
128
+ }
129
+ end
130
+ end
131
+
132
+ # Validates that the value of the specified slot is not in a particular enumerable object.
133
+ #
134
+ # Person = Meta.new do
135
+ # validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here"
136
+ # validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60"
137
+ # validates_exclusion_of :format, :in => %w( mov avi ), :message => 'extension #{slotvalue} is not allowed'
138
+ # end
139
+ #
140
+ # Configuration options:
141
+ # * <tt>in</tt> - An enumerable object of items that the value shouldn't be part of
142
+ # * <tt>message</tt> - Specifies a customer error message (default is: "is reserved")
143
+ # * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false)
144
+ # * <tt>allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is: false)
145
+ # * <tt>if</tt> - Specifies a method or slot name to call to determine if the validation should
146
+ # occur (e.g. :if => :allow_validation, or :if => 'signup_step_less_than_three'). The
147
+ # method result or slot should be equal to a true or false value.
148
+ # * <tt>unless</tt> - Specifies a method or slot name to call to determine if the validation should
149
+ # not occur (e.g. :unless => :skip_validation, or :unless => 'signup_step_less_than_three'). The
150
+ # method result or slot should be equal to a true or false value.
151
+ def validates_exclusion_of(slotname, opts={})
152
+ register_validation("exclusion_of", slotname, opts, :not_excluded) do |opts|
153
+ raise ArgumentError, "validates_exclusion_of requires :in set" unless opts['in']
154
+ raise ArgumentError, "object must respond to the method include?" unless opts['in'].respond_to? :include?
155
+
156
+ {
157
+ :in => opts['in'],
158
+ :allow_nil => !!opts['allow_nil'],
159
+ :allow_blank => !!opts['allow_blank']
160
+ }
161
+ end
162
+ end
163
+
164
+ # Validates whether the value of the specified attribute is numeric by trying to convert it to
165
+ # a float with Kernel.Float (if <tt>only_integer</tt> is false) or applying it to the regular expression
166
+ # <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>only_integer</tt> is set to true).
167
+ #
168
+ # Item = Meta.new do
169
+ # validates_numericality_of :price
170
+ # end
171
+ #
172
+ # * <tt>message</tt> - A custom error message (default is: "is not a number")
173
+ # * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
174
+ # * <tt>only_integer</tt> Specifies whether the value has to be an integer, e.g. an integral value (default is false)
175
+ # * <tt>allow_nil</tt> Skip validation if attribute is nil (default is
176
+ # false). Notice that for fixnum and float columns empty strings are converted to nil
177
+ # * <tt>greater_than</tt> Specifies the value must be greater than the supplied value
178
+ # * <tt>greater_than_or_equal_to</tt> Specifies the value must be greater than or equal the supplied value
179
+ # * <tt>equal_to</tt> Specifies the value must be equal to the supplied value
180
+ # * <tt>less_than</tt> Specifies the value must be less than the supplied value
181
+ # * <tt>less_than_or_equal_to</tt> Specifies the value must be less than or equal the supplied value
182
+ # * <tt>odd</tt> Specifies the value must be an odd number
183
+ # * <tt>even</tt> Specifies the value must be an even number
184
+ # * <tt>if</tt> - Specifies a method or slot name to call to determine if the validation should
185
+ # occur (e.g. :if => :allow_validation, or :if => 'signup_step_less_than_three'). The
186
+ # method result or slot should be equal to a true or false value.
187
+ # * <tt>unless</tt> - Specifies a method or slot name to call to determine if the validation should
188
+ # not occur (e.g. :unless => :skip_validation, or :unless => 'signup_step_less_than_three'). The
189
+ # method result or slot should be equal to a true or false value.
190
+ NUMERICALITY_CHECKS = { 'greater_than' => :>, 'greater_than_or_equal_to' => :>=,
191
+ 'equal_to' => :==, 'less_than' => :<, 'less_than_or_equal_to' => :<=,
192
+ 'odd' => :odd?, 'even' => :even? }.freeze
193
+
194
+ def validates_numericality_of(slotname, opts={})
195
+ register_validation("numericality_of", slotname, opts, nil) do |opts|
196
+ numeric_checks = opts.reject { |key, val| !NUMERICALITY_CHECKS.include? key }
197
+
198
+ %w(odd even).each do |o|
199
+ raise ArgumentError, ":#{o} must be set to true if set at all" if opts.include?(o) && opts[o] != true
200
+ end
201
+
202
+ (numeric_checks.keys - %w(odd even)).each do |option|
203
+ raise ArgumentError, "#{option} must be a number" unless opts[option].is_a? Numeric
204
+ end
205
+
206
+ {
207
+ :only_integer => opts['only_integer'],
208
+ :numeric_checks => numeric_checks,
209
+ :allow_nil => !!opts['allow_nil']
210
+ }
211
+ end
212
+ end
213
+
214
+ # Validates whether the value of the specified attribute is of the correct
215
+ # form by matching it against the regular expression provided.
216
+ #
217
+ # Person = Meta.new do
218
+ # validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create
219
+ # end
220
+ #
221
+ # Note: use \A and \Z to match the start and end of the string, ^ and $ match the start/end of a line.
222
+ #
223
+ # A regular expression must be provided or else an exception will be
224
+ # raised.
225
+ #
226
+ # Configuration options:
227
+ # * <tt>message</tt> - A custom error message (default is: "is invalid")
228
+ # * <tt>with</tt> - The regular expression used to validate the format with (note: must be supplied!)
229
+ # * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
230
+ # * <tt>if</tt> - Specifies a method or slot name to call to determine if the validation should
231
+ # occur (e.g. :if => :allow_validation, or :if => 'signup_step_less_than_three'). The
232
+ # method result or slot should be equal to a true or false value.
233
+ # * <tt>unless</tt> - Specifies a method or slot name to call to determine if the validation should
234
+ # not occur (e.g. :unless => :skip_validation, or :unless => 'signup_step_less_than_three'). The
235
+ # method result or slot should be equal to a true or false value.
236
+ def validates_format_of(slotname, opts={})
237
+ register_validation("format_of", slotname, opts, :invalid_format) do |opts|
238
+ unless regexp = opts['with'].is_a?(Regexp)
239
+ raise ArgumentError, "validates_format_of requires :with => regexp"
240
+ end
241
+ { :with => opts['with'] }
242
+ end
243
+ end
244
+
245
+ # Encapsulates the pattern of wanting to validate a password or email
246
+ # address field with a confirmation. Example:
247
+ #
248
+ # Model:
249
+ # Person = Meta.new do
250
+ # validates_confirmation_of :password
251
+ # validates_confirmation_of :email_address, :message => "should match confirmation"
252
+ # end
253
+ #
254
+ # View:
255
+ # <%= password_field "person", "password" %>
256
+ # <%= password_field "person", "password_confirmation" %>
257
+ #
258
+ # The added +password_confirmation+ slot is virtual; it exists only as
259
+ # an in-memory slot for validating the password. To achieve this, the
260
+ # validation adds accessors to the model for the confirmation slot.
261
+ # NOTE: This check is performed only if +password_confirmation+ is not nil,
262
+ # and by default only on save. To require confirmation, make sure to add a
263
+ # presence check for the confirmation attribute:
264
+ #
265
+ # validates_presence_of :password_confirmation, :if => :password_changed?
266
+ #
267
+ # Configuration options:
268
+ # * <tt>message</tt> - A custom error message (default is: "doesn't match confirmation")
269
+ # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
270
+ # * <tt>if</tt> - Specifies a method or slot name to call to determine if the validation should
271
+ # occur (e.g. :if => :allow_validation, or :if => 'signup_step_less_than_three'). The
272
+ # method result or slot should be equal to a true or false value.
273
+ # * <tt>unless</tt> - Specifies a method or slot name to call to determine if the validation should
274
+ # not occur (e.g. :unless => :skip_validation, or :unless => 'signup_step_less_than_three'). The
275
+ # method result or slot should be equal to a true or false value.
276
+ def validates_confirmation_of(slotname, opts = {})
277
+ register_validation("confirmation_of", slotname, opts, :not_confirmed)
278
+
279
+ virtualizes(slotname.to_s + "_confirmation")
280
+ end
281
+
282
+ # Encapsulates the pattern of wanting to validate the acceptance of a terms
283
+ # of service check box (or similar agreement). Example:
284
+ #
285
+ # Person = Meta.new do
286
+ # validates_acceptance_of :terms_of_service
287
+ # validates_acceptance_of :eula, :message => "must be abided"
288
+ # end
289
+ #
290
+ # The terms_of_service and eula slots are virtualized. This check is
291
+ # performed only if terms_of_service is not nil and by default on save.
292
+ #
293
+ # Configuration options:
294
+ # * <tt>message</tt> - A custom error message (default is: "must be accepted")
295
+ # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
296
+ # * <tt>allow_nil</tt> - Skip validation if attribute is nil. (default is true)
297
+ # * <tt>accept</tt> - Specifies value that is considered accepted. The default value is a string "1", which
298
+ # makes it easy to relate to an HTML checkbox. This should be set to 'true' if you are validating a database
299
+ # column, since the attribute is typecast from "1" to <tt>true</tt> before validation.
300
+ # * <tt>if</tt> - Specifies a method or slot name to call to determine if the validation should
301
+ # occur (e.g. :if => :allow_validation, or :if => 'signup_step_less_than_three'). The
302
+ # method result or slot should be equal to a true or false value.
303
+ # * <tt>unless</tt> - Specifies a method or slot name to call to determine if the validation should
304
+ # not occur (e.g. :unless => :skip_validation, or :unless => 'signup_step_less_than_three'). The
305
+ # method result or slot should be equal to a true or false value.
306
+ def validates_acceptance_of(slotname, opts = {})
307
+ register_validation("acceptance_of", slotname, opts, :not_accepted) do |opts|
308
+ allow_nil = opts['allow_nil'].nil? ? true : !!opts['allow_nil']
309
+ accept = opts['accept'] || "1"
310
+
311
+ { :allow_nil => allow_nil, :accept => accept }
312
+ end
313
+
314
+ virtualizes slotname.to_s
315
+ end
316
+
317
+ # Validates that the specified slot matches the length restrictions
318
+ # supplied. Only one option can be used at a time:
319
+ #
320
+ # Person = Meta.new do
321
+ # validates_length_of :first_name, :maximum=>30
322
+ # validates_length_of :last_name, :maximum=>30, :message=>"less than %d if you don't mind"
323
+ # validates_length_of :fax, :in => 7..32, :allow_nil => true
324
+ # validates_length_of :phone, :in => 7..32, :allow_blank => true
325
+ # validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name"
326
+ # validates_length_of :fav_bra_size, :minimum=>1, :too_short=>"please enter at least %d character"
327
+ # validates_length_of :smurf_leader, :is=>4, :message=>"papa is spelled with %d characters... don't play me."
328
+ # end
329
+ #
330
+ # Configuration options:
331
+ # * <tt>minimum</tt> - The minimum size of the attribute
332
+ # * <tt>maximum</tt> - The maximum size of the attribute
333
+ # * <tt>is</tt> - The exact size of the attribute
334
+ # * <tt>within</tt> - A range specifying the minimum and maximum size of the attribute
335
+ # * <tt>in</tt> - A synonym(or alias) for :within
336
+ # * <tt>allow_nil</tt> - Attribute may be nil; skip validation.
337
+ # * <tt>allow_blank</tt> - Attribute may be blank; skip validation.
338
+ #
339
+ # * <tt>too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %d characters)")
340
+ # * <tt>too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)")
341
+ # * <tt>wrong_length</tt> - The error message if using the :is method and the attribute is the wrong size (default is: "is the wrong length (should be %d characters)")
342
+ # * <tt>message</tt> - The error message to use for a :minimum, :maximum, or :is violation. An alias of the appropriate too_long/too_short/wrong_length message
343
+ # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
344
+ # * <tt>if</tt> - Specifies a method or slot name to call to determine if the validation should
345
+ # occur (e.g. :if => :allow_validation, or :if => 'signup_step_less_than_three'). The
346
+ # method result or slot should be equal to a true or false value.
347
+ # * <tt>unless</tt> - Specifies a method or slot name to call to determine if the validation should
348
+ # not occur (e.g. :unless => :skip_validation, or :unless => 'signup_step_less_than_three'). The
349
+ # method result or slot should be equal to a true or false value.
350
+ RANGE_OPTIONS = %w(is within in minimum maximum).freeze unless defined? RANGE_OPTIONS
351
+ RANGE_VALIDATIONS = {
352
+ 'is' => [ :==, ERROR_MESSAGES[:wrong_length] ],
353
+ 'minimum' => [ :>=, ERROR_MESSAGES[:too_short] ],
354
+ 'maximum' => [ :<=, ERROR_MESSAGES[:too_long] ]
355
+ }.freeze unless defined? RANGE_VALIDATIONS
356
+
357
+ def validates_length_of(slotname, opts = {})
358
+ register_validation("length_of", slotname, opts, nil) do |opts|
359
+ range_options = opts.reject { |opt, val| !RANGE_OPTIONS.include? opt }
360
+
361
+ case range_options.size
362
+ when 0
363
+ raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.'
364
+ when 1
365
+ # Valid number of options; do nothing.
366
+ else
367
+ raise ArgumentError, 'Too many range options specified. Choose only one.'
368
+ end
369
+
370
+ ropt = range_options.keys.first
371
+ ropt_value = range_options[ropt]
372
+
373
+ opthash = {
374
+ :allow_nil => !!opts['allow_nil'],
375
+ :allow_blank => !!opts['allow_blank']
376
+ }
377
+
378
+ case ropt
379
+ when 'within', 'in'
380
+ raise ArgumentError, ":#{ropt} must be a Range" unless ropt_value.is_a? Range
381
+
382
+ opthash[:too_short] = (opts['too_short'] || ERROR_MESSAGES[:too_short]) % ropt_value.begin
383
+ opthash[:too_long] = (opts['too_long'] || ERROR_MESSAGES[:too_long]) % ropt_value.end
384
+ opthash[:range] = ropt_value
385
+
386
+ when 'is', 'minimum', 'maximum'
387
+ raise ArgumentError, ":#{ropt} must be a nonnegative Integer" unless ropt_value.is_a?(Integer) and ropt_value >= 0
388
+
389
+ # Declare different validations per option.
390
+ opthash[:message] = (opts['message'] || RANGE_VALIDATIONS[ropt][1]) % ropt_value
391
+ opthash[:method] = RANGE_VALIDATIONS[ropt][0]
392
+ opthash[:argument] = ropt_value
393
+ end
394
+
395
+ opthash
396
+ end
397
+ end
398
+
399
+ alias_method :validates_size_of, :validates_length_of
400
+
401
+ # Validates whether the associated object or objects are all valid
402
+ # themselves. Works with any kind of association.
403
+ #
404
+ # Book = Meta.new do
405
+ # has_many :pages
406
+ #
407
+ # validates_associated :pages, :library
408
+ # end
409
+ #
410
+ # Warning: If, after the above definition, you then wrote:
411
+ #
412
+ # Page = Meta.new do
413
+ # belongs_to :book
414
+ #
415
+ # validates_associated :book
416
+ # end
417
+ #
418
+ # ...this would specify a circular dependency and cause infinite recursion.
419
+ #
420
+ # NOTE: This validation will not fail if the association hasn't been
421
+ # assigned. If you want to ensure that the association is both present and
422
+ # guaranteed to be valid, you also need to use validates_presence_of (and,
423
+ # possibly, validates_type_of).
424
+ #
425
+ # Configuration options:
426
+ # * <tt>message</tt> - A custom error message (default is: "is invalid")
427
+ # * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
428
+ # * <tt>if</tt> - Specifies a method or slot name to call to determine if the validation should
429
+ # occur (e.g. :if => :allow_validation, or :if => 'signup_step_less_than_three'). The
430
+ # method result or slot should be equal to a true or false value.
431
+ # * <tt>unless</tt> - Specifies a method or slot name to call to determine if the validation should
432
+ # not occur (e.g. :unless => :skip_validation, or :unless => 'signup_step_less_than_three'). The
433
+ # method result or slot should be equal to a true or false value.
434
+ def validates_associated(slotname, opts = {})
435
+ register_validation('associated', slotname, opts, :invalid)
436
+ end
437
+
438
+ # this module gets mixed into Document
439
+ module InstanceMethods
440
+ class Errors
441
+ include Enumerable
442
+
443
+ def initialize(base)
444
+ @base, @errors = base, {}
445
+ end
446
+
447
+ def add(slot, msg)
448
+ slot = slot.to_s
449
+ @errors[slot] = [] if @errors[slot].nil?
450
+ @errors[slot] << msg
451
+ end
452
+
453
+ def invalid?(slot)
454
+ !@errors[slot.to_s].nil?
455
+ end
456
+
457
+ def on(slot)
458
+ errors = @errors[slot.to_s]
459
+ return nil if errors.nil?
460
+ errors.size == 1 ? errors.first : errors
461
+ end
462
+
463
+ alias :[] :on
464
+
465
+ # Returns true if no errors have been added.
466
+ def empty?
467
+ @errors.empty?
468
+ end
469
+
470
+ # Removes all errors that have been added.
471
+ def clear
472
+ @errors = {}
473
+ end
474
+
475
+ # Returns all the error messages in an array.
476
+ def messages
477
+ @errors.values.inject([]) { |error_messages, slot| error_messages + slot }
478
+ end
479
+
480
+ # Returns the total number of errors added. Two errors added to the
481
+ # same slot will be counted as such.
482
+ def size
483
+ @errors.values.inject(0) { |error_count, slot| error_count + slot.size }
484
+ end
485
+
486
+ # Yields each attribute and associated message per error added
487
+ def each
488
+ @errors.each_key do |slot|
489
+ @errors[slot].each do |msg|
490
+ yield [ msg ]
491
+ end
492
+ end
493
+ end
494
+
495
+ alias_method :count, :size
496
+ alias_method :length, :size
497
+ end
498
+
499
+ # Runs validations and returns true if no errors were added otherwise false.
500
+ def valid?
501
+ errors.clear
502
+
503
+ execute_callbacks :on_validation
504
+
505
+ errors.empty?
506
+ end
507
+
508
+ # Returns the Errors object that holds all information about attribute
509
+ # error messages.
510
+ def errors
511
+ @errors ||= Errors.new(self)
512
+ end
513
+ end
514
+
515
+ private
516
+
517
+ def register_validation(validation_name, slotname, opts, message)
518
+ opts = opts.stringify_keys
519
+ slotname = slotname.to_s
520
+ on = (opts['on'] || 'save').to_s.downcase
521
+ message = opts['message'] || (message.is_a?(Symbol) ? ERROR_MESSAGES[message] : message)
522
+
523
+ check_condition(opts['if']) if opts['if']
524
+ check_condition(opts['unless']) if opts['unless']
525
+
526
+ options_hash = {
527
+ :slotname => slotname,
528
+ :message => message,
529
+ :on => on,
530
+ :if => opts['if'],
531
+ :unless => opts['unless']
532
+ }
533
+
534
+ options_hash.merge!(yield(opts)) if block_given?
535
+
536
+ validation_slot = "validates_#{validation_name}_#{slotname}"
537
+
538
+ @meta_initialization_procs << Proc.new do
539
+ @args.last.reverse_merge!(validation_slot => { :meta => name }.merge(options_hash))
540
+ end
541
+ end
542
+
543
+ NUMERICALITY_ERRORS = {
544
+ 'greater_than' => '#{slotname} must be greater than %d',
545
+ 'greater_than_or_equal_to' => '#{slotname} must be greater than or equal to %d',
546
+ 'equal_to' => '#{slotname} must be equal to %d',
547
+ 'less_than' => '#{slotname} must be less than %d',
548
+ 'less_than_or_equal_to' => '#{slotname} must be less than or equal to %d',
549
+ 'odd' => '#{slotname} must be odd',
550
+ 'even' => '#{slotname} must be even'
551
+ }.freeze unless defined? NUMERICALITY_ERRORS
552
+
553
+ def initialize_validations
554
+ install_validations_for(:validates_presence_of) do |doc, validation, slotname|
555
+ doc.has_slot? slotname
556
+ end
557
+
558
+ install_validations_for(:validates_type_of) do |doc, validation, slotname|
559
+ doc[slotname].is_a? Kernel.const_get(validation[:validation_type])
560
+ end
561
+
562
+ install_validations_for(:validates_inclusion_of) do |doc, validation, slotname|
563
+ validation[:in].include? doc[slotname]
564
+ end
565
+
566
+ install_validations_for(:validates_exclusion_of) do |doc, validation, slotname|
567
+ !validation[:in].include?(doc[slotname])
568
+ end
569
+
570
+ install_validations_for(:validates_format_of) do |doc, validation, slotname|
571
+ !(doc[slotname] !~ validation[:with])
572
+ end
573
+
574
+ install_validations_for(:validates_uniqueness_of) do |doc, validation, slotname|
575
+ meta = Kernel.const_get(doc.meta.name)
576
+
577
+ !(found = meta.find(slotname.to_sym => doc[slotname])) ||
578
+ (found.size == 0) ||
579
+ (found.first.uuid == doc.uuid)
580
+ end
581
+
582
+ # using lambda here enables us to use return
583
+ numericality = lambda do |doc, validation, slotname|
584
+ value = doc[slotname]
585
+
586
+ if validation[:only_integer]
587
+ return (validation[:message] || "#{slotname} must be integer") unless value.to_s =~ /\A[+-]?\d+\Z/
588
+ value = value.to_i
589
+ else
590
+ value = Kernel.Float(value) rescue false
591
+ return (validation[:message] || "#{slotname} is not a number") unless value
592
+ end
593
+
594
+ errors = []
595
+
596
+ validation[:numeric_checks].each do |option, optvalue|
597
+ testresult = if %w(odd even).include? option
598
+ value.to_i.send(NUMERICALITY_CHECKS[option])
599
+ else
600
+ value.send(NUMERICALITY_CHECKS[option], optvalue)
601
+ end
602
+
603
+ unless testresult
604
+ errors << ((validation[:message] || NUMERICALITY_ERRORS[option]) % optvalue)
605
+ end
606
+ end
607
+
608
+ errors.empty? ? true : errors
609
+ end
610
+
611
+ install_validations_for(:validates_numericality_of, &numericality)
612
+ install_validations_for(:validates_confirmation_of) do |doc, validation, slotname|
613
+ confirm_slotname = slotname + "_confirmation"
614
+ !doc.has_slot?(confirm_slotname) || doc[slotname] == doc[confirm_slotname]
615
+ end
616
+
617
+ install_validations_for(:validates_acceptance_of) do |doc, validation, slotname|
618
+ doc[slotname] == validation[:accept]
619
+ end
620
+
621
+ install_validations_for(:validates_length_of) do |doc, validation, slotname|
622
+ value = doc[slotname]
623
+ size = case value
624
+ when nil then 0
625
+ when String then value.split(//).size
626
+ else
627
+ value.size
628
+ end
629
+
630
+ if range = validation[:range]
631
+ if value.nil? or size < range.begin
632
+ validation[:too_short]
633
+ elsif size > range.end
634
+ validation[:too_long]
635
+ else
636
+ true
637
+ end
638
+ else
639
+ !value.nil? && size.send(validation[:method], validation[:argument])
640
+ end
641
+ end
642
+
643
+ install_validations_for(:validates_associated) do |doc, validation, slotname|
644
+ begin
645
+ result = false
646
+ Util.catch_circular_reference(doc,:validates_associated_reference_stack) do
647
+ if doc.has_slot?(slotname)
648
+ val = doc[slotname]
649
+
650
+ if val.respond_to? :inject
651
+ result = val.inject(true) { |prev, associate| prev && (associate.respond_to?(:valid?) ? associate.valid? : true) }
652
+ else
653
+ result = val.respond_to?(:valid?) ? val.valid? : true
654
+ end
655
+ else
656
+ result = true
657
+ end
658
+ end
659
+ rescue Util::CircularReferenceCondition
660
+ result = true
661
+ end
662
+ result
663
+ end
664
+ end
665
+
666
+ def install_validations_for(sym, &block)
667
+ on_validation(sym) do |doc|
668
+ grep_slots(doc, sym.to_s + "_") do |slotname_to_validate, meta_slotname|
669
+ next unless validation = doc.meta[meta_slotname]
670
+
671
+ on = validation['on']
672
+
673
+ next unless (on == 'create' && doc.new?) || (on == 'update' && !doc.new?) || on == 'save'
674
+
675
+ next if validation[:if] && !evaluate_condition(validation[:if], doc)
676
+ next if validation[:unless] && evaluate_condition(validation[:unless], doc)
677
+
678
+ value = doc[slotname_to_validate]
679
+
680
+ next if validation[:allow_nil] && value.nil?
681
+ next if validation[:allow_blank] && value.blank?
682
+
683
+ case validation_result = block.call(doc, validation, slotname_to_validate)
684
+ when true then next
685
+ when false
686
+ add_error(doc, validation, slotname_to_validate, validation[:message])
687
+ when String
688
+ add_error(doc, validation, slotname_to_validate, validation_result)
689
+ when Array
690
+ validation_result.each { |message| add_error(doc, validation, slotname_to_validate, message) }
691
+ end
692
+ end
693
+ end
694
+ end
695
+
696
+ def add_error(doc, validation, slotname, message)
697
+ os = OpenStruct.new(validation)
698
+ os.document = doc
699
+ os.slotvalue = doc[slotname]
700
+
701
+ doc.errors.add(slotname, os.instance_eval("\"#{message}\""))
702
+ end
703
+ end
704
+ end