bigrecord 0.0.5

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 (104) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +44 -0
  3. data/Rakefile +17 -0
  4. data/VERSION +1 -0
  5. data/doc/bigrecord_specs.rdoc +36 -0
  6. data/doc/getting_started.rdoc +157 -0
  7. data/examples/bigrecord.yml +25 -0
  8. data/generators/bigrecord/bigrecord_generator.rb +17 -0
  9. data/generators/bigrecord/templates/bigrecord.rake +47 -0
  10. data/generators/bigrecord_migration/bigrecord_migration_generator.rb +13 -0
  11. data/generators/bigrecord_migration/templates/migration.rb +9 -0
  12. data/generators/bigrecord_model/bigrecord_model_generator.rb +28 -0
  13. data/generators/bigrecord_model/templates/migration.rb +13 -0
  14. data/generators/bigrecord_model/templates/model.rb +7 -0
  15. data/generators/bigrecord_model/templates/model_spec.rb +12 -0
  16. data/init.rb +9 -0
  17. data/install.rb +22 -0
  18. data/lib/big_record/abstract_base.rb +1088 -0
  19. data/lib/big_record/action_view_extensions.rb +266 -0
  20. data/lib/big_record/ar_associations/association_collection.rb +194 -0
  21. data/lib/big_record/ar_associations/association_proxy.rb +158 -0
  22. data/lib/big_record/ar_associations/belongs_to_association.rb +57 -0
  23. data/lib/big_record/ar_associations/belongs_to_many_association.rb +57 -0
  24. data/lib/big_record/ar_associations/has_and_belongs_to_many_association.rb +164 -0
  25. data/lib/big_record/ar_associations/has_many_association.rb +191 -0
  26. data/lib/big_record/ar_associations/has_one_association.rb +80 -0
  27. data/lib/big_record/ar_associations.rb +1608 -0
  28. data/lib/big_record/ar_reflection.rb +223 -0
  29. data/lib/big_record/attribute_methods.rb +75 -0
  30. data/lib/big_record/base.rb +618 -0
  31. data/lib/big_record/br_associations/association_collection.rb +194 -0
  32. data/lib/big_record/br_associations/association_proxy.rb +153 -0
  33. data/lib/big_record/br_associations/belongs_to_association.rb +52 -0
  34. data/lib/big_record/br_associations/belongs_to_many_association.rb +293 -0
  35. data/lib/big_record/br_associations/cached_item_proxy.rb +194 -0
  36. data/lib/big_record/br_associations/cached_item_proxy_factory.rb +62 -0
  37. data/lib/big_record/br_associations/has_and_belongs_to_many_association.rb +168 -0
  38. data/lib/big_record/br_associations/has_one_association.rb +80 -0
  39. data/lib/big_record/br_associations.rb +978 -0
  40. data/lib/big_record/br_reflection.rb +151 -0
  41. data/lib/big_record/callbacks.rb +367 -0
  42. data/lib/big_record/connection_adapters/abstract/connection_specification.rb +279 -0
  43. data/lib/big_record/connection_adapters/abstract/database_statements.rb +175 -0
  44. data/lib/big_record/connection_adapters/abstract/quoting.rb +58 -0
  45. data/lib/big_record/connection_adapters/abstract_adapter.rb +190 -0
  46. data/lib/big_record/connection_adapters/column.rb +491 -0
  47. data/lib/big_record/connection_adapters/hbase_adapter.rb +432 -0
  48. data/lib/big_record/connection_adapters/view.rb +27 -0
  49. data/lib/big_record/connection_adapters.rb +10 -0
  50. data/lib/big_record/deletion.rb +73 -0
  51. data/lib/big_record/dynamic_schema.rb +92 -0
  52. data/lib/big_record/embedded.rb +71 -0
  53. data/lib/big_record/embedded_associations/association_proxy.rb +148 -0
  54. data/lib/big_record/family_span_columns.rb +89 -0
  55. data/lib/big_record/fixtures.rb +1025 -0
  56. data/lib/big_record/migration.rb +380 -0
  57. data/lib/big_record/routing_ext.rb +65 -0
  58. data/lib/big_record/timestamp.rb +51 -0
  59. data/lib/big_record/validations.rb +830 -0
  60. data/lib/big_record.rb +125 -0
  61. data/lib/bigrecord.rb +1 -0
  62. data/rails/init.rb +9 -0
  63. data/spec/connections/bigrecord.yml +13 -0
  64. data/spec/connections/cassandra/connection.rb +2 -0
  65. data/spec/connections/hbase/connection.rb +2 -0
  66. data/spec/debug.log +281 -0
  67. data/spec/integration/br_associations_spec.rb +80 -0
  68. data/spec/lib/animal.rb +12 -0
  69. data/spec/lib/book.rb +10 -0
  70. data/spec/lib/broken_migrations/duplicate_name/20090706182535_add_animals_table.rb +14 -0
  71. data/spec/lib/broken_migrations/duplicate_name/20090706193019_add_animals_table.rb +9 -0
  72. data/spec/lib/broken_migrations/duplicate_version/20090706190623_add_books_table.rb +9 -0
  73. data/spec/lib/broken_migrations/duplicate_version/20090706190623_add_companies_table.rb +9 -0
  74. data/spec/lib/company.rb +14 -0
  75. data/spec/lib/embedded/web_link.rb +12 -0
  76. data/spec/lib/employee.rb +33 -0
  77. data/spec/lib/migrations/20090706182535_add_animals_table.rb +13 -0
  78. data/spec/lib/migrations/20090706190623_add_books_table.rb +15 -0
  79. data/spec/lib/migrations/20090706193019_add_companies_table.rb +14 -0
  80. data/spec/lib/migrations/20090706194512_add_employees_table.rb +13 -0
  81. data/spec/lib/migrations/20090706195741_add_zoos_table.rb +13 -0
  82. data/spec/lib/novel.rb +5 -0
  83. data/spec/lib/zoo.rb +17 -0
  84. data/spec/spec.opts +4 -0
  85. data/spec/spec_helper.rb +55 -0
  86. data/spec/unit/abstract_base_spec.rb +287 -0
  87. data/spec/unit/adapters/abstract_adapter_spec.rb +56 -0
  88. data/spec/unit/adapters/adapter_shared_spec.rb +51 -0
  89. data/spec/unit/adapters/hbase_adapter_spec.rb +15 -0
  90. data/spec/unit/ar_associations_spec.rb +8 -0
  91. data/spec/unit/base_spec.rb +6 -0
  92. data/spec/unit/br_associations_spec.rb +58 -0
  93. data/spec/unit/embedded_spec.rb +43 -0
  94. data/spec/unit/find_spec.rb +34 -0
  95. data/spec/unit/hash_helper_spec.rb +44 -0
  96. data/spec/unit/migration_spec.rb +144 -0
  97. data/spec/unit/model_spec.rb +315 -0
  98. data/spec/unit/validations_spec.rb +182 -0
  99. data/tasks/bigrecord_tasks.rake +47 -0
  100. data/tasks/data_store.rb +46 -0
  101. data/tasks/gem.rb +22 -0
  102. data/tasks/rdoc.rb +8 -0
  103. data/tasks/spec.rb +34 -0
  104. metadata +189 -0
@@ -0,0 +1,830 @@
1
+ module BigRecord
2
+ # Raised by save! and create! when the record is invalid. Use the
3
+ # record method to retrieve the record which did not validate.
4
+ # begin
5
+ # complex_operation_that_calls_save!_internally
6
+ # rescue ActiveRecord::RecordInvalid => invalid
7
+ # puts invalid.record.errors
8
+ # end
9
+ class RecordInvalid < BigRecordError #:nodoc:
10
+ attr_reader :record
11
+ def initialize(record)
12
+ @record = record
13
+ super("Validation failed: #{@record.errors.full_messages.join(", ")}")
14
+ end
15
+ end
16
+
17
+ # Active Record validation is reported to and from this object, which is used by Base#save to
18
+ # determine whether the object in a valid state to be saved. See usage example in Validations.
19
+ class Errors
20
+ include Enumerable
21
+
22
+ def initialize(base) # :nodoc:
23
+ @base, @errors = base, {}
24
+ end
25
+
26
+ @@default_error_messages = {
27
+ :inclusion => "is not included in the list",
28
+ :exclusion => "is reserved",
29
+ :invalid => "is invalid",
30
+ :confirmation => "doesn't match confirmation",
31
+ :accepted => "must be accepted",
32
+ :empty => "can't be empty",
33
+ :blank => "can't be blank",
34
+ :too_long => "is too long (maximum is %d characters)",
35
+ :too_short => "is too short (minimum is %d characters)",
36
+ :wrong_length => "is the wrong length (should be %d characters)",
37
+ :taken => "has already been taken",
38
+ :not_a_number => "is not a number"
39
+ }
40
+
41
+ # Holds a hash with all the default error messages, such that they can be replaced by your own copy or localizations.
42
+ cattr_accessor :default_error_messages
43
+
44
+
45
+ # Adds an error to the base object instead of any particular attribute. This is used
46
+ # to report errors that don't tie to any specific attribute, but rather to the object
47
+ # as a whole. These error messages don't get prepended with any field name when iterating
48
+ # with each_full, so they should be complete sentences.
49
+ def add_to_base(msg)
50
+ add(:base, msg)
51
+ end
52
+
53
+ # Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
54
+ # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one
55
+ # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
56
+ # If no +msg+ is supplied, "invalid" is assumed.
57
+ def add(attribute, msg = @@default_error_messages[:invalid])
58
+ @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
59
+ @errors[attribute.to_s] << msg
60
+ end
61
+
62
+ # Will add an error message to each of the attributes in +attributes+ that is empty.
63
+ def add_on_empty(attributes, msg = @@default_error_messages[:empty])
64
+ for attr in [attributes].flatten
65
+ value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
66
+ is_empty = value.respond_to?("empty?") ? value.empty? : false
67
+ add(attr, msg) unless !value.nil? && !is_empty
68
+ end
69
+ end
70
+
71
+ # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?).
72
+ def add_on_blank(attributes, msg = @@default_error_messages[:blank])
73
+ for attr in [attributes].flatten
74
+ value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
75
+ add(attr, msg) if value.blank?
76
+ end
77
+ end
78
+
79
+ # Returns true if the specified +attribute+ has errors associated with it.
80
+ def invalid?(attribute)
81
+ !@errors[attribute.to_s].nil?
82
+ end
83
+
84
+ # * Returns nil, if no errors are associated with the specified +attribute+.
85
+ # * Returns the error message, if one error is associated with the specified +attribute+.
86
+ # * Returns an array of error messages, if more than one error is associated with the specified +attribute+.
87
+ def on(attribute)
88
+ errors = @errors[attribute.to_s]
89
+ return nil if errors.nil?
90
+ errors.size == 1 ? errors.first : errors
91
+ end
92
+
93
+ alias :[] :on
94
+
95
+ # Returns errors assigned to base object through add_to_base according to the normal rules of on(attribute).
96
+ def on_base
97
+ on(:base)
98
+ end
99
+
100
+ # Yields each attribute and associated message per error added.
101
+ def each
102
+ @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
103
+ end
104
+
105
+ # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
106
+ # through iteration as "First name can't be empty".
107
+ def each_full
108
+ full_messages.each { |msg| yield msg }
109
+ end
110
+
111
+ # Returns all the full error messages in an array.
112
+ def full_messages
113
+ full_messages = []
114
+
115
+ @errors.each_key do |attr|
116
+ @errors[attr].each do |msg|
117
+ next if msg.nil?
118
+
119
+ if attr == "base"
120
+ full_messages << msg
121
+ else
122
+ full_messages << @base.human_attribute_name(attr) + " " + msg
123
+ end
124
+ end
125
+ end
126
+ full_messages
127
+ end
128
+
129
+ # Returns true if no errors have been added.
130
+ def empty?
131
+ @errors.empty?
132
+ end
133
+
134
+ # Removes all the errors that have been added.
135
+ def clear
136
+ @errors = {}
137
+ end
138
+
139
+ # Returns the total number of errors added. Two errors added to the same attribute will be counted as such
140
+ # with this as well.
141
+ def size
142
+ @errors.values.inject(0) { |error_count, attribute| error_count + attribute.size }
143
+ end
144
+
145
+ alias_method :count, :size
146
+ alias_method :length, :size
147
+
148
+ # Return an XML representation of this error object.
149
+ def to_xml(options={})
150
+ options[:root] ||= "errors"
151
+ options[:indent] ||= 2
152
+ options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
153
+
154
+ options[:builder].instruct! unless options.delete(:skip_instruct)
155
+ options[:builder].errors do |e|
156
+ full_messages.each { |msg| e.error(msg) }
157
+ end
158
+ end
159
+ end
160
+
161
+
162
+ # Active Records implement validation by overwriting Base#validate (or the variations, +validate_on_create+ and
163
+ # +validate_on_update+). Each of these methods can inspect the state of the object, which usually means ensuring
164
+ # that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression).
165
+ #
166
+ # Example:
167
+ #
168
+ # class Person < ActiveRecord::Base
169
+ # protected
170
+ # def validate
171
+ # errors.add_on_empty %w( first_name last_name )
172
+ # errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
173
+ # end
174
+ #
175
+ # def validate_on_create # is only run the first time a new object is saved
176
+ # unless valid_discount?(membership_discount)
177
+ # errors.add("membership_discount", "has expired")
178
+ # end
179
+ # end
180
+ #
181
+ # def validate_on_update
182
+ # errors.add_to_base("No changes have occurred") if unchanged_attributes?
183
+ # end
184
+ # end
185
+ #
186
+ # person = Person.new("first_name" => "David", "phone_number" => "what?")
187
+ # person.save # => false (and doesn't do the save)
188
+ # person.errors.empty? # => false
189
+ # person.errors.count # => 2
190
+ # person.errors.on "last_name" # => "can't be empty"
191
+ # person.errors.on "phone_number" # => "has invalid format"
192
+ # person.errors.each_full { |msg| puts msg }
193
+ # # => "Last name can't be empty\n" +
194
+ # "Phone number has invalid format"
195
+ #
196
+ # person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" }
197
+ # person.save # => true (and person is now saved in the database)
198
+ #
199
+ # An +Errors+ object is automatically created for every Active Record.
200
+ #
201
+ # Please do have a look at ActiveRecord::Validations::ClassMethods for a higher level of validations.
202
+ module Validations
203
+ VALIDATIONS = %w( validate validate_on_create validate_on_update )
204
+
205
+ def self.included(base) # :nodoc:
206
+ base.extend ClassMethods
207
+ base.class_eval do
208
+ alias_method_chain :save, :validation
209
+ alias_method_chain :save!, :validation
210
+ alias_method_chain :update_attribute, :validation_skipping
211
+ end
212
+ end
213
+
214
+ # All of the following validations are defined in the class scope of the model that you're interested in validating.
215
+ # They offer a more declarative way of specifying when the model is valid and when it is not. It is recommended to use
216
+ # these over the low-level calls to validate and validate_on_create when possible.
217
+ module ClassMethods
218
+ DEFAULT_VALIDATION_OPTIONS = {
219
+ :on => :save,
220
+ :allow_nil => false,
221
+ :message => nil
222
+ }.freeze
223
+
224
+ ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze
225
+
226
+ def validate(*methods, &block)
227
+ methods << block if block_given?
228
+ write_inheritable_set(:validate, methods)
229
+ end
230
+
231
+ def validate_on_create(*methods, &block)
232
+ methods << block if block_given?
233
+ write_inheritable_set(:validate_on_create, methods)
234
+ end
235
+
236
+ def validate_on_update(*methods, &block)
237
+ methods << block if block_given?
238
+ write_inheritable_set(:validate_on_update, methods)
239
+ end
240
+
241
+ def condition_block?(condition)
242
+ condition.respond_to?("call") && (condition.arity == 1 || condition.arity == -1)
243
+ end
244
+
245
+ # Determine from the given condition (whether a block, procedure, method or string)
246
+ # whether or not to validate the record. See #validates_each.
247
+ def evaluate_condition(condition, record)
248
+ case condition
249
+ when Symbol: record.send(condition)
250
+ when String: eval(condition, binding)
251
+ else
252
+ if condition_block?(condition)
253
+ condition.call(record)
254
+ else
255
+ raise(
256
+ BigRecordError,
257
+ "Validations need to be either a symbol, string (to be eval'ed), proc/method, or " +
258
+ "class implementing a static validation method"
259
+ )
260
+ end
261
+ end
262
+ end
263
+
264
+ # Validates each attribute against a block.
265
+ #
266
+ # class Person < ActiveRecord::Base
267
+ # validates_each :first_name, :last_name do |record, attr, value|
268
+ # record.errors.add attr, 'starts with z.' if value[0] == ?z
269
+ # end
270
+ # end
271
+ #
272
+ # Options:
273
+ # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
274
+ # * <tt>allow_nil</tt> - Skip validation if attribute is nil.
275
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
276
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
277
+ # method, proc or string should return or evaluate to a true or false value.
278
+ def validates_each(*attrs)
279
+ options = attrs.last.is_a?(Hash) ? attrs.pop.symbolize_keys : {}
280
+ attrs = attrs.flatten
281
+
282
+ # Declare the validation.
283
+ send(validation_method(options[:on] || :save)) do |record|
284
+ # Don't validate when there is an :if condition and that condition is false
285
+ unless options[:if] && !evaluate_condition(options[:if], record)
286
+ attrs.each do |attr|
287
+ value = record.send(attr)
288
+ next if value.nil? && options[:allow_nil]
289
+ yield record, attr, value
290
+ end
291
+ end
292
+ end
293
+ end
294
+
295
+ # Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example:
296
+ #
297
+ # Model:
298
+ # class Person < ActiveRecord::Base
299
+ # validates_confirmation_of :user_name, :password
300
+ # validates_confirmation_of :email_address, :message => "should match confirmation"
301
+ # end
302
+ #
303
+ # View:
304
+ # <%= password_field "person", "password" %>
305
+ # <%= password_field "person", "password_confirmation" %>
306
+ #
307
+ # The person has to already have a password attribute (a column in the people table), but the password_confirmation is virtual.
308
+ # It exists only as an in-memory variable for validating the password. This check is performed only if password_confirmation
309
+ # is not nil and by default on save.
310
+ #
311
+ # Configuration options:
312
+ # * <tt>message</tt> - A custom error message (default is: "doesn't match confirmation")
313
+ # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
314
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
315
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
316
+ # method, proc or string should return or evaluate to a true or false value.
317
+ def validates_confirmation_of(*attr_names)
318
+ configuration = { :message => BigRecord::Errors.default_error_messages[:confirmation], :on => :save }
319
+ configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
320
+
321
+ attr_accessor *(attr_names.map { |n| "#{n}_confirmation" })
322
+
323
+ validates_each(attr_names, configuration) do |record, attr_name, value|
324
+ record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
325
+ end
326
+ end
327
+
328
+ # Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example:
329
+ #
330
+ # class Person < ActiveRecord::Base
331
+ # validates_acceptance_of :terms_of_service
332
+ # validates_acceptance_of :eula, :message => "must be abided"
333
+ # end
334
+ #
335
+ # The terms_of_service attribute is entirely virtual. No database column is needed. This check is performed only if
336
+ # terms_of_service is not nil and by default on save.
337
+ #
338
+ # Configuration options:
339
+ # * <tt>message</tt> - A custom error message (default is: "must be accepted")
340
+ # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
341
+ # * <tt>accept</tt> - Specifies value that is considered accepted. The default value is a string "1", which
342
+ # makes it easy to relate to an HTML checkbox.
343
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
344
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
345
+ # method, proc or string should return or evaluate to a true or false value.
346
+ def validates_acceptance_of(*attr_names)
347
+ configuration = { :message => BigRecord::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" }
348
+ configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
349
+
350
+ attr_accessor *attr_names
351
+
352
+ validates_each(attr_names,configuration) do |record, attr_name, value|
353
+ record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept]
354
+ end
355
+ end
356
+
357
+ # Validates that the specified attributes are not blank (as defined by Object#blank?). Happens by default on save. Example:
358
+ #
359
+ # class Person < ActiveRecord::Base
360
+ # validates_presence_of :first_name
361
+ # end
362
+ #
363
+ # The first_name attribute must be in the object and it cannot be blank.
364
+ #
365
+ # If you want to validate the presence of a boolean field (where the real values are true and false),
366
+ # you will want to use validates_inclusion_of :field_name, :in => [true, false]
367
+ # This is due to the way Object#blank? handles boolean values. false.blank? # => true
368
+ #
369
+ # Configuration options:
370
+ # * <tt>message</tt> - A custom error message (default is: "can't be blank")
371
+ # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
372
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
373
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
374
+ # method, proc or string should return or evaluate to a true or false value.
375
+ #
376
+ # === Warning
377
+ # Validate the presence of the foreign key, not the instance variable itself.
378
+ # Do this:
379
+ # validate_presence_of :invoice_id
380
+ #
381
+ # Not this:
382
+ # validate_presence_of :invoice
383
+ #
384
+ # If you validate the presence of the associated object, you will get
385
+ # failures on saves when both the parent object and the child object are
386
+ # new.
387
+ def validates_presence_of(*attr_names)
388
+ configuration = { :message => BigRecord::Errors.default_error_messages[:blank], :on => :save }
389
+ configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
390
+
391
+ # can't use validates_each here, because it cannot cope with nonexistent attributes,
392
+ # while errors.add_on_empty can
393
+ attr_names.each do |attr_name|
394
+ send(validation_method(configuration[:on])) do |record|
395
+ unless configuration[:if] and not evaluate_condition(configuration[:if], record)
396
+ record.errors.add_on_blank(attr_name,configuration[:message])
397
+ end
398
+ end
399
+ end
400
+ end
401
+
402
+ # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time:
403
+ #
404
+ # class Person < ActiveRecord::Base
405
+ # validates_length_of :first_name, :maximum=>30
406
+ # validates_length_of :last_name, :maximum=>30, :message=>"less than %d if you don't mind"
407
+ # validates_length_of :fax, :in => 7..32, :allow_nil => true
408
+ # validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name"
409
+ # validates_length_of :fav_bra_size, :minimum=>1, :too_short=>"please enter at least %d character"
410
+ # validates_length_of :smurf_leader, :is=>4, :message=>"papa is spelled with %d characters... don't play me."
411
+ # end
412
+ #
413
+ # Configuration options:
414
+ # * <tt>minimum</tt> - The minimum size of the attribute
415
+ # * <tt>maximum</tt> - The maximum size of the attribute
416
+ # * <tt>is</tt> - The exact size of the attribute
417
+ # * <tt>within</tt> - A range specifying the minimum and maximum size of the attribute
418
+ # * <tt>in</tt> - A synonym(or alias) for :within
419
+ # * <tt>allow_nil</tt> - Attribute may be nil; skip validation.
420
+ #
421
+ # * <tt>too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %d characters)")
422
+ # * <tt>too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)")
423
+ # * <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)")
424
+ # * <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
425
+ # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
426
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
427
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
428
+ # method, proc or string should return or evaluate to a true or false value.
429
+ def validates_length_of(*attrs)
430
+ # Merge given options with defaults.
431
+ options = {
432
+ :too_long => BigRecord::Errors.default_error_messages[:too_long],
433
+ :too_short => BigRecord::Errors.default_error_messages[:too_short],
434
+ :wrong_length => BigRecord::Errors.default_error_messages[:wrong_length]
435
+ }.merge(DEFAULT_VALIDATION_OPTIONS)
436
+ options.update(attrs.pop.symbolize_keys) if attrs.last.is_a?(Hash)
437
+
438
+ # Ensure that one and only one range option is specified.
439
+ range_options = ALL_RANGE_OPTIONS & options.keys
440
+ case range_options.size
441
+ when 0
442
+ raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.'
443
+ when 1
444
+ # Valid number of options; do nothing.
445
+ else
446
+ raise ArgumentError, 'Too many range options specified. Choose only one.'
447
+ end
448
+
449
+ # Get range option and value.
450
+ option = range_options.first
451
+ option_value = options[range_options.first]
452
+
453
+ case option
454
+ when :within, :in
455
+ raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)
456
+
457
+ too_short = options[:too_short] % option_value.begin
458
+ too_long = options[:too_long] % option_value.end
459
+
460
+ validates_each(attrs, options) do |record, attr, value|
461
+ if value.nil? or value.split(//).size < option_value.begin
462
+ record.errors.add(attr, too_short)
463
+ elsif value.split(//).size > option_value.end
464
+ record.errors.add(attr, too_long)
465
+ end
466
+ end
467
+ when :is, :minimum, :maximum
468
+ raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0
469
+
470
+ # Declare different validations per option.
471
+ validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
472
+ message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }
473
+
474
+ message = (options[:message] || options[message_options[option]]) % option_value
475
+
476
+ validates_each(attrs, options) do |record, attr, value|
477
+ if value.kind_of?(String)
478
+ record.errors.add(attr, message) unless !value.nil? and value.split(//).size.send(validity_checks[option], option_value)
479
+ else
480
+ record.errors.add(attr, message) unless !value.nil? and value.size.send(validity_checks[option], option_value)
481
+ end
482
+ end
483
+ end
484
+ end
485
+
486
+ alias_method :validates_size_of, :validates_length_of
487
+
488
+
489
+ # Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user
490
+ # can be named "davidhh".
491
+ #
492
+ # class Person < ActiveRecord::Base
493
+ # validates_uniqueness_of :user_name, :scope => :account_id
494
+ # end
495
+ #
496
+ # It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example,
497
+ # making sure that a teacher can only be on the schedule once per semester for a particular class.
498
+ #
499
+ # class TeacherSchedule < ActiveRecord::Base
500
+ # validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id]
501
+ # end
502
+ #
503
+ # When the record is created, a check is performed to make sure that no record exists in the database with the given value for the specified
504
+ # attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself.
505
+ #
506
+ # Configuration options:
507
+ # * <tt>message</tt> - Specifies a custom error message (default is: "has already been taken")
508
+ # * <tt>scope</tt> - One or more columns by which to limit the scope of the uniquness constraint.
509
+ # * <tt>case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (true by default).
510
+ # * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false)
511
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
512
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
513
+ # method, proc or string should return or evaluate to a true or false value.
514
+
515
+ def validates_uniqueness_of(*attr_names)
516
+ configuration = { :message => BigRecord::Errors.default_error_messages[:taken], :case_sensitive => true }
517
+ configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
518
+
519
+ validates_each(attr_names,configuration) do |record, attr_name, value|
520
+ if value.nil? || (configuration[:case_sensitive] || !columns_hash[attr_name.to_s].text?)
521
+ condition_sql = "#{record.class.table_name}.#{attr_name} #{attribute_condition(value)}"
522
+ condition_params = [value]
523
+ else
524
+ condition_sql = "LOWER(#{record.class.table_name}.#{attr_name}) #{attribute_condition(value)}"
525
+ condition_params = [value.downcase]
526
+ end
527
+ if scope = configuration[:scope]
528
+ Array(scope).map do |scope_item|
529
+ scope_value = record.send(scope_item)
530
+ condition_sql << " AND #{record.class.table_name}.#{scope_item} #{attribute_condition(scope_value)}"
531
+ condition_params << scope_value
532
+ end
533
+ end
534
+ unless record.new_record?
535
+ condition_sql << " AND #{record.class.table_name}.#{record.class.primary_key} <> ?"
536
+ condition_params << record.send(:id)
537
+ end
538
+ if record.class.find(:first, :conditions => [condition_sql, *condition_params])
539
+ record.errors.add(attr_name, configuration[:message])
540
+ end
541
+ end
542
+ end
543
+
544
+
545
+
546
+ # Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression
547
+ # provided.
548
+ #
549
+ # class Person < ActiveRecord::Base
550
+ # validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create
551
+ # end
552
+ #
553
+ # Note: use \A and \Z to match the start and end of the string, ^ and $ match the start/end of a line.
554
+ #
555
+ # A regular expression must be provided or else an exception will be raised.
556
+ #
557
+ # Configuration options:
558
+ # * <tt>message</tt> - A custom error message (default is: "is invalid")
559
+ # * <tt>with</tt> - The regular expression used to validate the format with (note: must be supplied!)
560
+ # * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
561
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
562
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
563
+ # method, proc or string should return or evaluate to a true or false value.
564
+ def validates_format_of(*attr_names)
565
+ configuration = { :message => BigRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil }
566
+ configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
567
+
568
+ raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp)
569
+
570
+ validates_each(attr_names, configuration) do |record, attr_name, value|
571
+ record.errors.add(attr_name, configuration[:message]) unless value.to_s =~ configuration[:with]
572
+ end
573
+ end
574
+
575
+ # Validates whether the value of the specified attribute is available in a particular enumerable object.
576
+ #
577
+ # class Person < ActiveRecord::Base
578
+ # validates_inclusion_of :gender, :in=>%w( m f ), :message=>"woah! what are you then!??!!"
579
+ # validates_inclusion_of :age, :in=>0..99
580
+ # end
581
+ #
582
+ # Configuration options:
583
+ # * <tt>in</tt> - An enumerable object of available items
584
+ # * <tt>message</tt> - Specifies a customer error message (default is: "is not included in the list")
585
+ # * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false)
586
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
587
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
588
+ # method, proc or string should return or evaluate to a true or false value.
589
+ def validates_inclusion_of(*attr_names)
590
+ configuration = { :message => BigRecord::Errors.default_error_messages[:inclusion], :on => :save }
591
+ configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
592
+
593
+ enum = configuration[:in] || configuration[:within]
594
+
595
+ raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?")
596
+
597
+ validates_each(attr_names, configuration) do |record, attr_name, value|
598
+ record.errors.add(attr_name, configuration[:message]) unless enum.include?(value)
599
+ end
600
+ end
601
+
602
+ # Validates that the value of the specified attribute is not in a particular enumerable object.
603
+ #
604
+ # class Person < ActiveRecord::Base
605
+ # validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here"
606
+ # validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60"
607
+ # end
608
+ #
609
+ # Configuration options:
610
+ # * <tt>in</tt> - An enumerable object of items that the value shouldn't be part of
611
+ # * <tt>message</tt> - Specifies a customer error message (default is: "is reserved")
612
+ # * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false)
613
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
614
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
615
+ # method, proc or string should return or evaluate to a true or false value.
616
+ def validates_exclusion_of(*attr_names)
617
+ configuration = { :message => BigRecord::Errors.default_error_messages[:exclusion], :on => :save }
618
+ configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
619
+
620
+ enum = configuration[:in] || configuration[:within]
621
+
622
+ raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?")
623
+
624
+ validates_each(attr_names, configuration) do |record, attr_name, value|
625
+ record.errors.add(attr_name, configuration[:message]) if enum.include?(value)
626
+ end
627
+ end
628
+
629
+ # Validates whether the associated object or objects are all valid themselves. Works with any kind of association.
630
+ #
631
+ # class Book < ActiveRecord::Base
632
+ # has_many :pages
633
+ # belongs_to :library
634
+ #
635
+ # validates_associated :pages, :library
636
+ # end
637
+ #
638
+ # Warning: If, after the above definition, you then wrote:
639
+ #
640
+ # class Page < ActiveRecord::Base
641
+ # belongs_to :book
642
+ #
643
+ # validates_associated :book
644
+ # end
645
+ #
646
+ # ...this would specify a circular dependency and cause infinite recursion.
647
+ #
648
+ # NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association
649
+ # is both present and guaranteed to be valid, you also need to use validates_presence_of.
650
+ #
651
+ # Configuration options:
652
+ # * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
653
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
654
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
655
+ # method, proc or string should return or evaluate to a true or false value.
656
+ def validates_associated(*attr_names)
657
+ configuration = { :message => BigRecord::Errors.default_error_messages[:invalid], :on => :save }
658
+ configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
659
+
660
+ validates_each(attr_names, configuration) do |record, attr_name, value|
661
+ record.errors.add(attr_name, configuration[:message]) unless
662
+ (value.is_a?(Array) ? value : [value]).all? { |r| r.nil? or r.valid? }
663
+ end
664
+ end
665
+
666
+ # Validates whether the value of the specified attribute is numeric by trying to convert it to
667
+ # a float with Kernel.Float (if <tt>integer</tt> is false) or applying it to the regular expression
668
+ # <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>integer</tt> is set to true).
669
+ #
670
+ # class Person < ActiveRecord::Base
671
+ # validates_numericality_of :value, :on => :create
672
+ # end
673
+ #
674
+ # Configuration options:
675
+ # * <tt>message</tt> - A custom error message (default is: "is not a number")
676
+ # * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
677
+ # * <tt>only_integer</tt> Specifies whether the value has to be an integer, e.g. an integral value (default is false)
678
+ # * <tt>allow_nil</tt> Skip validation if attribute is nil (default is false). Notice that for fixnum and float columns empty strings are converted to nil
679
+ # * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
680
+ # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
681
+ # method, proc or string should return or evaluate to a true or false value.
682
+ def validates_numericality_of(*attr_names)
683
+ configuration = { :message => BigRecord::Errors.default_error_messages[:not_a_number], :on => :save,
684
+ :only_integer => false, :allow_nil => false }
685
+ configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
686
+
687
+ if configuration[:only_integer]
688
+ validates_each(attr_names,configuration) do |record, attr_name,value|
689
+ record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_before_type_cast").to_s =~ /\A[+-]?\d+\Z/
690
+ end
691
+ else
692
+ validates_each(attr_names,configuration) do |record, attr_name,value|
693
+ next if configuration[:allow_nil] and record.send("#{attr_name}_before_type_cast").nil?
694
+ begin
695
+ Kernel.Float(record.send("#{attr_name}_before_type_cast").to_s)
696
+ rescue ArgumentError, TypeError
697
+ record.errors.add(attr_name, configuration[:message])
698
+ end
699
+ end
700
+ end
701
+ end
702
+
703
+
704
+ # Creates an object just like Base.create but calls save! instead of save
705
+ # so an exception is raised if the record is invalid.
706
+ def create!(attributes = nil)
707
+ if attributes.is_a?(Array)
708
+ attributes.collect { |attr| create!(attr) }
709
+ else
710
+ attributes ||= {}
711
+ attributes.reverse_merge!(scope(:create)) if scoped?(:create)
712
+
713
+ object = new(attributes)
714
+ object.save!
715
+ object
716
+ end
717
+ end
718
+
719
+
720
+ private
721
+ def write_inheritable_set(key, methods)
722
+ existing_methods = read_inheritable_attribute(key) || []
723
+ write_inheritable_attribute(key, methods | existing_methods)
724
+ end
725
+
726
+ def validation_method(on)
727
+ case on
728
+ when :save then :validate
729
+ when :create then :validate_on_create
730
+ when :update then :validate_on_update
731
+ end
732
+ end
733
+ end
734
+
735
+ # The validation process on save can be skipped by passing false. The regular Base#save method is
736
+ # replaced with this when the validations module is mixed in, which it is by default.
737
+ def save_with_validation(perform_validation = true)
738
+ if perform_validation && valid? || !perform_validation
739
+ save_without_validation
740
+ else
741
+ false
742
+ end
743
+ end
744
+
745
+ # Attempts to save the record just like Base#save but will raise a RecordInvalid exception instead of returning false
746
+ # if the record is not valid.
747
+ def save_with_validation!
748
+ if valid?
749
+ save_without_validation!
750
+ else
751
+ raise RecordInvalid.new(self)
752
+ end
753
+ end
754
+
755
+ # Updates a single attribute and saves the record without going through the normal validation procedure.
756
+ # This is especially useful for boolean flags on existing records. The regular +update_attribute+ method
757
+ # in Base is replaced with this when the validations module is mixed in, which it is by default.
758
+ def update_attribute_with_validation_skipping(name, value)
759
+ send(name.to_s + '=', value)
760
+ save(false)
761
+ end
762
+
763
+ # Runs validate and validate_on_create or validate_on_update and returns true if no errors were added otherwise false.
764
+ def valid?
765
+ errors.clear
766
+
767
+ run_validations(:validate)
768
+ validate
769
+
770
+ if new_record?
771
+ run_validations(:validate_on_create)
772
+ validate_on_create
773
+ else
774
+ run_validations(:validate_on_update)
775
+ validate_on_update
776
+ end
777
+
778
+ errors.empty?
779
+ end
780
+
781
+ # Returns the Errors object that holds all information about attribute error messages.
782
+ def errors
783
+ @errors ||= Errors.new(self)
784
+ end
785
+
786
+ protected
787
+ # Overwrite this method for validation checks on all saves and use Errors.add(field, msg) for invalid attributes.
788
+ def validate #:doc:
789
+ end
790
+
791
+ # Overwrite this method for validation checks used only on creation.
792
+ def validate_on_create #:doc:
793
+ end
794
+
795
+ # Overwrite this method for validation checks used only on updates.
796
+ def validate_on_update # :doc:
797
+ end
798
+
799
+ private
800
+ def run_validations(validation_method)
801
+ validations = self.class.read_inheritable_attribute(validation_method.to_sym)
802
+ if validations.nil? then return end
803
+ validations.each do |validation|
804
+ if validation.is_a?(Symbol)
805
+ self.send(validation)
806
+ elsif validation.is_a?(String)
807
+ eval(validation, binding)
808
+ elsif validation_block?(validation)
809
+ validation.call(self)
810
+ elsif validation_class?(validation, validation_method)
811
+ validation.send(validation_method, self)
812
+ else
813
+ raise(
814
+ BigRecord,
815
+ "Validations need to be either a symbol, string (to be eval'ed), proc/method, or " +
816
+ "class implementing a static validation method"
817
+ )
818
+ end
819
+ end
820
+ end
821
+
822
+ def validation_block?(validation)
823
+ validation.respond_to?("call") && (validation.arity == 1 || validation.arity == -1)
824
+ end
825
+
826
+ def validation_class?(validation, validation_method)
827
+ validation.respond_to?(validation_method)
828
+ end
829
+ end
830
+ end