strokedb 0.0.2

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.
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