ardm-validations 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (153) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +35 -0
  3. data/.travis.yml +11 -0
  4. data/Gemfile +51 -0
  5. data/LICENSE +21 -0
  6. data/README.rdoc +122 -0
  7. data/Rakefile +4 -0
  8. data/ardm-validations.gemspec +28 -0
  9. data/lib/ardm-validations.rb +1 -0
  10. data/lib/dm-validations.rb +169 -0
  11. data/lib/dm-validations/auto_validate.rb +252 -0
  12. data/lib/dm-validations/context.rb +66 -0
  13. data/lib/dm-validations/contextual_validators.rb +220 -0
  14. data/lib/dm-validations/exceptions.rb +5 -0
  15. data/lib/dm-validations/formats/email.rb +65 -0
  16. data/lib/dm-validations/formats/url.rb +27 -0
  17. data/lib/dm-validations/support/object.rb +18 -0
  18. data/lib/dm-validations/support/ordered_hash.rb +434 -0
  19. data/lib/dm-validations/validation_errors.rb +137 -0
  20. data/lib/dm-validations/validators/absent_field_validator.rb +58 -0
  21. data/lib/dm-validations/validators/acceptance_validator.rb +79 -0
  22. data/lib/dm-validations/validators/block_validator.rb +61 -0
  23. data/lib/dm-validations/validators/confirmation_validator.rb +92 -0
  24. data/lib/dm-validations/validators/format_validator.rb +124 -0
  25. data/lib/dm-validations/validators/generic_validator.rb +184 -0
  26. data/lib/dm-validations/validators/length_validator.rb +249 -0
  27. data/lib/dm-validations/validators/method_validator.rb +64 -0
  28. data/lib/dm-validations/validators/numeric_validator.rb +182 -0
  29. data/lib/dm-validations/validators/primitive_validator.rb +58 -0
  30. data/lib/dm-validations/validators/required_field_validator.rb +83 -0
  31. data/lib/dm-validations/validators/uniqueness_validator.rb +67 -0
  32. data/lib/dm-validations/validators/within_validator.rb +74 -0
  33. data/lib/dm-validations/version.rb +5 -0
  34. data/spec/fixtures/barcode.rb +40 -0
  35. data/spec/fixtures/basketball_court.rb +58 -0
  36. data/spec/fixtures/basketball_player.rb +34 -0
  37. data/spec/fixtures/beta_tester_account.rb +33 -0
  38. data/spec/fixtures/bill_of_landing.rb +47 -0
  39. data/spec/fixtures/boat_dock.rb +26 -0
  40. data/spec/fixtures/city.rb +24 -0
  41. data/spec/fixtures/company.rb +93 -0
  42. data/spec/fixtures/corporate_world.rb +39 -0
  43. data/spec/fixtures/country.rb +24 -0
  44. data/spec/fixtures/ethernet_frame.rb +56 -0
  45. data/spec/fixtures/event.rb +44 -0
  46. data/spec/fixtures/g3_concert.rb +57 -0
  47. data/spec/fixtures/jabberwock.rb +27 -0
  48. data/spec/fixtures/kayak.rb +28 -0
  49. data/spec/fixtures/lernean_hydra.rb +39 -0
  50. data/spec/fixtures/llama_spaceship.rb +15 -0
  51. data/spec/fixtures/mathematical_function.rb +34 -0
  52. data/spec/fixtures/memory_object.rb +30 -0
  53. data/spec/fixtures/mittelschnauzer.rb +39 -0
  54. data/spec/fixtures/motor_launch.rb +21 -0
  55. data/spec/fixtures/multibyte.rb +16 -0
  56. data/spec/fixtures/page.rb +32 -0
  57. data/spec/fixtures/phone_number.rb +28 -0
  58. data/spec/fixtures/pirogue.rb +28 -0
  59. data/spec/fixtures/programming_language.rb +83 -0
  60. data/spec/fixtures/reservation.rb +38 -0
  61. data/spec/fixtures/scm_operation.rb +56 -0
  62. data/spec/fixtures/sms_message.rb +22 -0
  63. data/spec/fixtures/udp_packet.rb +49 -0
  64. data/spec/integration/absent_field_validator/absent_field_validator_spec.rb +90 -0
  65. data/spec/integration/absent_field_validator/spec_helper.rb +7 -0
  66. data/spec/integration/acceptance_validator/acceptance_validator_spec.rb +196 -0
  67. data/spec/integration/acceptance_validator/spec_helper.rb +7 -0
  68. data/spec/integration/automatic_validation/custom_messages_for_inferred_validation_spec.rb +57 -0
  69. data/spec/integration/automatic_validation/disabling_inferred_validation_spec.rb +49 -0
  70. data/spec/integration/automatic_validation/inferred_boolean_properties_validation_spec.rb +100 -0
  71. data/spec/integration/automatic_validation/inferred_float_property_validation_spec.rb +45 -0
  72. data/spec/integration/automatic_validation/inferred_format_validation_spec.rb +35 -0
  73. data/spec/integration/automatic_validation/inferred_integer_properties_validation_spec.rb +70 -0
  74. data/spec/integration/automatic_validation/inferred_length_validation_spec.rb +142 -0
  75. data/spec/integration/automatic_validation/inferred_presence_validation_spec.rb +45 -0
  76. data/spec/integration/automatic_validation/inferred_primitive_validation_spec.rb +22 -0
  77. data/spec/integration/automatic_validation/inferred_uniqueness_validation_spec.rb +52 -0
  78. data/spec/integration/automatic_validation/inferred_within_validation_spec.rb +39 -0
  79. data/spec/integration/automatic_validation/spec_helper.rb +57 -0
  80. data/spec/integration/block_validator/block_validator_spec.rb +32 -0
  81. data/spec/integration/block_validator/spec_helper.rb +5 -0
  82. data/spec/integration/conditional_validation/if_condition_spec.rb +63 -0
  83. data/spec/integration/conditional_validation/spec_helper.rb +5 -0
  84. data/spec/integration/confirmation_validator/confirmation_validator_spec.rb +76 -0
  85. data/spec/integration/confirmation_validator/spec_helper.rb +5 -0
  86. data/spec/integration/datamapper_models/association_validation_spec.rb +29 -0
  87. data/spec/integration/datamapper_models/inheritance_spec.rb +82 -0
  88. data/spec/integration/dirty_attributes/dirty_attributes_spec.rb +13 -0
  89. data/spec/integration/duplicated_validations/duplicated_validations_spec.rb +24 -0
  90. data/spec/integration/duplicated_validations/spec_helper.rb +5 -0
  91. data/spec/integration/format_validator/email_format_validator_spec.rb +139 -0
  92. data/spec/integration/format_validator/format_validator_spec.rb +64 -0
  93. data/spec/integration/format_validator/regexp_validator_spec.rb +33 -0
  94. data/spec/integration/format_validator/spec_helper.rb +5 -0
  95. data/spec/integration/format_validator/url_format_validator_spec.rb +93 -0
  96. data/spec/integration/length_validator/default_value_spec.rb +14 -0
  97. data/spec/integration/length_validator/equality_spec.rb +87 -0
  98. data/spec/integration/length_validator/error_message_spec.rb +22 -0
  99. data/spec/integration/length_validator/maximum_spec.rb +49 -0
  100. data/spec/integration/length_validator/minimum_spec.rb +54 -0
  101. data/spec/integration/length_validator/range_spec.rb +87 -0
  102. data/spec/integration/length_validator/spec_helper.rb +7 -0
  103. data/spec/integration/method_validator/method_validator_spec.rb +241 -0
  104. data/spec/integration/method_validator/spec_helper.rb +5 -0
  105. data/spec/integration/numeric_validator/equality_with_float_type_spec.rb +65 -0
  106. data/spec/integration/numeric_validator/equality_with_integer_type_spec.rb +41 -0
  107. data/spec/integration/numeric_validator/float_type_spec.rb +90 -0
  108. data/spec/integration/numeric_validator/gt_with_float_type_spec.rb +37 -0
  109. data/spec/integration/numeric_validator/gte_with_float_type_spec.rb +37 -0
  110. data/spec/integration/numeric_validator/integer_only_true_spec.rb +91 -0
  111. data/spec/integration/numeric_validator/integer_type_spec.rb +86 -0
  112. data/spec/integration/numeric_validator/lt_with_float_type_spec.rb +37 -0
  113. data/spec/integration/numeric_validator/lte_with_float_type_spec.rb +37 -0
  114. data/spec/integration/numeric_validator/spec_helper.rb +5 -0
  115. data/spec/integration/primitive_validator/primitive_validator_spec.rb +92 -0
  116. data/spec/integration/primitive_validator/spec_helper.rb +5 -0
  117. data/spec/integration/pure_ruby_objects/plain_old_ruby_object_validation_spec.rb +118 -0
  118. data/spec/integration/required_field_validator/association_spec.rb +72 -0
  119. data/spec/integration/required_field_validator/boolean_type_value_spec.rb +155 -0
  120. data/spec/integration/required_field_validator/date_type_value_spec.rb +127 -0
  121. data/spec/integration/required_field_validator/datetime_type_value_spec.rb +127 -0
  122. data/spec/integration/required_field_validator/float_type_value_spec.rb +131 -0
  123. data/spec/integration/required_field_validator/integer_type_value_spec.rb +99 -0
  124. data/spec/integration/required_field_validator/plain_old_ruby_object_spec.rb +35 -0
  125. data/spec/integration/required_field_validator/shared_examples.rb +26 -0
  126. data/spec/integration/required_field_validator/spec_helper.rb +7 -0
  127. data/spec/integration/required_field_validator/string_type_value_spec.rb +167 -0
  128. data/spec/integration/required_field_validator/text_type_value_spec.rb +49 -0
  129. data/spec/integration/shared/default_validation_context.rb +13 -0
  130. data/spec/integration/shared/valid_and_invalid_model.rb +35 -0
  131. data/spec/integration/uniqueness_validator/spec_helper.rb +5 -0
  132. data/spec/integration/uniqueness_validator/uniqueness_validator_spec.rb +116 -0
  133. data/spec/integration/within_validator/spec_helper.rb +5 -0
  134. data/spec/integration/within_validator/within_validator_spec.rb +168 -0
  135. data/spec/public/resource_spec.rb +105 -0
  136. data/spec/rcov.opts +6 -0
  137. data/spec/spec.opts +4 -0
  138. data/spec/spec_helper.rb +29 -0
  139. data/spec/unit/contextual_validators/emptiness_spec.rb +50 -0
  140. data/spec/unit/contextual_validators/execution_spec.rb +48 -0
  141. data/spec/unit/contextual_validators/spec_helper.rb +37 -0
  142. data/spec/unit/generic_validator/equality_operator_spec.rb +26 -0
  143. data/spec/unit/generic_validator/optional_spec.rb +54 -0
  144. data/spec/unit/validation_errors/adding_spec.rb +54 -0
  145. data/spec/unit/validation_errors/emptiness_spec.rb +38 -0
  146. data/spec/unit/validation_errors/enumerable_spec.rb +32 -0
  147. data/spec/unit/validation_errors/reading_spec.rb +35 -0
  148. data/spec/unit/validation_errors/respond_to_spec.rb +15 -0
  149. data/spec/unit/validators/within_validator_spec.rb +23 -0
  150. data/tasks/spec.rake +38 -0
  151. data/tasks/yard.rake +9 -0
  152. data/tasks/yardstick.rake +19 -0
  153. metadata +256 -0
@@ -0,0 +1,252 @@
1
+ module DataMapper
2
+ # for options_with_message
3
+ Property.accept_options :message, :messages, :set, :validates, :auto_validation, :format
4
+
5
+ module Validations
6
+ module AutoValidations
7
+
8
+ module ModelExtension
9
+ # @api private
10
+ def property(*)
11
+ property = super
12
+ AutoValidations.generate_for_property(property)
13
+ # FIXME: explicit return needed for YARD to parse this properly
14
+ return property
15
+ end
16
+
17
+ Model.append_extensions self
18
+ end # module ModelExtension
19
+
20
+
21
+ # TODO: why are there 3 entry points to this ivar?
22
+ # #disable_auto_validations, #disabled_auto_validations?, #auto_validations_disabled?
23
+ attr_reader :disable_auto_validations
24
+
25
+ # Checks whether auto validations are currently
26
+ # disabled (see +disable_auto_validations+ method
27
+ # that takes a block)
28
+ #
29
+ # @return [TrueClass, FalseClass]
30
+ # true if auto validation is currently disabled
31
+ #
32
+ # @api semipublic
33
+ def disabled_auto_validations?
34
+ @disable_auto_validations || false
35
+ end
36
+
37
+ # TODO: deprecate all but one of these 3 variants
38
+ alias_method :auto_validations_disabled?, :disabled_auto_validations?
39
+
40
+ # disables generation of validations for
41
+ # duration of given block
42
+ #
43
+ # @api public
44
+ def without_auto_validations
45
+ previous, @disable_auto_validations = @disable_auto_validations, true
46
+ yield
47
+ ensure
48
+ @disable_auto_validations = previous
49
+ end
50
+
51
+ # Auto-generate validations for a given property. This will only occur
52
+ # if the option :auto_validation is either true or left undefined.
53
+ #
54
+ # Triggers that generate validator creation
55
+ #
56
+ # :required => true
57
+ # Setting the option :required to true causes a
58
+ # validates_presence_of validator to be automatically created on
59
+ # the property
60
+ #
61
+ # :length => 20
62
+ # Setting the option :length causes a validates_length_of
63
+ # validator to be automatically created on the property. If the
64
+ # value is a Integer the validation will set :maximum => value
65
+ # if the value is a Range the validation will set
66
+ # :within => value
67
+ #
68
+ # :format => :predefined / lambda / Proc
69
+ # Setting the :format option causes a validates_format_of
70
+ # validator to be automatically created on the property
71
+ #
72
+ # :set => ["foo", "bar", "baz"]
73
+ # Setting the :set option causes a validates_within
74
+ # validator to be automatically created on the property
75
+ #
76
+ # Integer type
77
+ # Using a Integer type causes a validates_numericality_of
78
+ # validator to be created for the property. integer_only
79
+ # is set to true
80
+ #
81
+ # BigDecimal or Float type
82
+ # Using a Integer type causes a validates_numericality_of
83
+ # validator to be created for the property. integer_only
84
+ # is set to false, and precision/scale match the property
85
+ #
86
+ #
87
+ # Messages
88
+ #
89
+ # :messages => {..}
90
+ # Setting :messages hash replaces standard error messages
91
+ # with custom ones. For instance:
92
+ # :messages => {:presence => "Field is required",
93
+ # :format => "Field has invalid format"}
94
+ # Hash keys are: :presence, :format, :length, :is_unique,
95
+ # :is_number, :is_primitive
96
+ #
97
+ # :message => "Some message"
98
+ # It is just shortcut if only one validation option is set
99
+ #
100
+ # @api private
101
+ def self.generate_for_property(property)
102
+ return if (property.model.disabled_auto_validations? ||
103
+ skip_auto_validation_for?(property))
104
+
105
+ # all auto-validations (aside from presence) should skip
106
+ # validation when the value is nil
107
+ opts = { :allow_nil => true }
108
+
109
+ if property.options.key?(:validates)
110
+ opts[:context] = property.options[:validates]
111
+ end
112
+
113
+ infer_presence_validation_for(property, opts.dup)
114
+ infer_length_validation_for(property, opts.dup)
115
+ infer_format_validation_for(property, opts.dup)
116
+ infer_uniqueness_validation_for(property, opts.dup)
117
+ infer_within_validation_for(property, opts.dup)
118
+ infer_type_validation_for(property, opts.dup)
119
+ end # generate_for_property
120
+
121
+ private
122
+
123
+ # Checks whether or not property should be auto validated.
124
+ # It is the case for properties with :auto_validation option
125
+ # given and it's value evaluates to true
126
+ #
127
+ # @return [TrueClass, FalseClass]
128
+ # true for properties with :auto_validation option that has
129
+ # positive value
130
+ #
131
+ # @api private
132
+ def self.skip_auto_validation_for?(property)
133
+ (property.options.key?(:auto_validation) &&
134
+ !property.options[:auto_validation])
135
+ end
136
+
137
+ # @api private
138
+ def self.infer_presence_validation_for(property, options)
139
+ return if skip_presence_validation?(property)
140
+
141
+ validation_options = options_with_message(options, property, :presence)
142
+ property.model.validates_presence_of property.name, validation_options
143
+ end
144
+
145
+ # @api private
146
+ def self.skip_presence_validation?(property)
147
+ property.allow_blank? || property.serial?
148
+ end
149
+
150
+ # @api private
151
+ def self.infer_length_validation_for(property, options)
152
+ return unless (property.kind_of?(DataMapper::Property::String) ||
153
+ property.kind_of?(DataMapper::Property::Text))
154
+
155
+ length = property.options.fetch(:length, DataMapper::Property::String::DEFAULT_LENGTH)
156
+
157
+
158
+ if length.is_a?(Range)
159
+ raise ArgumentError, "Infinity is no valid upper bound for a length range" if length.last == Infinity
160
+ options[:within] = length
161
+ else
162
+ options[:maximum] = length
163
+ end
164
+
165
+ validation_options = options_with_message(options, property, :length)
166
+ property.model.validates_length_of property.name, validation_options
167
+ end
168
+
169
+ # @api private
170
+ def self.infer_format_validation_for(property, options)
171
+ return unless property.options.key?(:format)
172
+
173
+ options[:with] = property.options[:format]
174
+
175
+ validation_options = options_with_message(options, property, :format)
176
+ property.model.validates_format_of property.name, validation_options
177
+ end
178
+
179
+ # @api private
180
+ def self.infer_uniqueness_validation_for(property, options)
181
+ return unless property.options.key?(:unique)
182
+
183
+ case value = property.options[:unique]
184
+ when Array, Symbol
185
+ options[:scope] = Array(value)
186
+
187
+ validation_options = options_with_message(options, property, :is_unique)
188
+ property.model.validates_uniqueness_of property.name, validation_options
189
+ when TrueClass
190
+ validation_options = options_with_message(options, property, :is_unique)
191
+ property.model.validates_uniqueness_of property.name, validation_options
192
+ end
193
+ end
194
+
195
+ # @api private
196
+ def self.infer_within_validation_for(property, options)
197
+ return unless property.options.key?(:set)
198
+
199
+ options[:set] = property.options[:set]
200
+
201
+ validation_options = options_with_message(options, property, :within)
202
+ property.model.validates_within property.name, validation_options
203
+ end
204
+
205
+ # @api private
206
+ def self.infer_type_validation_for(property, options)
207
+ return if property.respond_to?(:custom?) && property.custom?
208
+
209
+ if property.kind_of?(Property::Numeric)
210
+ options[:gte] = property.min if property.min
211
+ options[:lte] = property.max if property.max
212
+ end
213
+
214
+ if Integer == property.primitive
215
+ options[:integer_only] = true
216
+
217
+ validation_options = options_with_message(options, property, :is_number)
218
+ property.model.validates_numericality_of property.name, validation_options
219
+ elsif (BigDecimal == property.primitive ||
220
+ Float == property.primitive)
221
+ options[:precision] = property.precision
222
+ options[:scale] = property.scale
223
+
224
+ validation_options = options_with_message(options, property, :is_number)
225
+ property.model.validates_numericality_of property.name, validation_options
226
+ else
227
+ # We only need this in the case we don't already
228
+ # have a numeric validator, because otherwise
229
+ # it will cause duplicate validation errors
230
+ validation_options = options_with_message(options, property, :is_primitive)
231
+ property.model.validates_primitive_type_of property.name, validation_options
232
+ end
233
+ end
234
+
235
+ # adds message for validator
236
+ #
237
+ # @api private
238
+ def self.options_with_message(base_options, property, validator_name)
239
+ options = base_options.clone
240
+ opts = property.options
241
+
242
+ if opts.key?(:messages)
243
+ options[:message] = opts[:messages][validator_name]
244
+ elsif opts.key?(:message)
245
+ options[:message] = opts[:message]
246
+ end
247
+
248
+ options
249
+ end
250
+ end # module AutoValidations
251
+ end # module Validations
252
+ end # module DataMapper
@@ -0,0 +1,66 @@
1
+ module DataMapper
2
+ module Validations
3
+ # Module with validation context functionality.
4
+ #
5
+ # Contexts are implemented using a thread-local array-based stack.
6
+ #
7
+ module Context
8
+ # Execute a block of code within a specific validation context
9
+ #
10
+ # @param [Symbol] context
11
+ # the context to execute the block of code within
12
+ #
13
+ # @api semipublic
14
+ def self.in_context(context)
15
+ stack << context
16
+ yield
17
+ ensure
18
+ stack.pop
19
+ end
20
+
21
+ # Get the current validation context or nil (if no context is on the stack).
22
+ #
23
+ # @return [Symbol, NilClass]
24
+ # The current validation context (for the current thread),
25
+ # or nil if no current context is on the stack
26
+ def self.current
27
+ stack.last
28
+ end
29
+
30
+ # Are there any contexts on the stack?
31
+ #
32
+ # @return [Boolean]
33
+ # true/false whether there are any contexts on the context stack
34
+ #
35
+ # @api semipublic
36
+ def self.any?(&block)
37
+ stack.any?(&block)
38
+ end
39
+
40
+ # The (thread-local) validation context stack
41
+ # This allows object graphs to be saved within potentially nested contexts
42
+ # without having to pass the validation context throughout
43
+ #
44
+ # @api private
45
+ def self.stack
46
+ Thread.current[:dm_validations_context_stack] ||= []
47
+ end
48
+
49
+ # The default validation context for this Resource.
50
+ # This Resource's default context can be overridden by implementing
51
+ # #default_validation_context
52
+ #
53
+ # @return [Symbol]
54
+ # the current validation context from the context stack
55
+ # (if valid for this model), or :default
56
+ #
57
+ # @api semipublic
58
+ def default_validation_context
59
+ model.validators.current_context || :default
60
+ end
61
+
62
+ end # module Context
63
+
64
+ include Context
65
+ end # module Validations
66
+ end # module DataMapper
@@ -0,0 +1,220 @@
1
+ require 'forwardable'
2
+
3
+ module DataMapper
4
+ module Validations
5
+ #
6
+ # @author Guy van den Berg
7
+ # @since 0.9
8
+ class ContextualValidators
9
+ extend Forwardable
10
+
11
+ #
12
+ # Delegators
13
+ #
14
+
15
+ def_delegators :@contexts, :empty?, :each
16
+ def_delegators :@attributes, :[]
17
+ include Enumerable
18
+
19
+ attr_reader :contexts, :attributes
20
+
21
+ def initialize(model = nil)
22
+ @model = model
23
+ @contexts = {}
24
+ @attributes = {}
25
+ end
26
+
27
+ #
28
+ # API
29
+ #
30
+
31
+ # Return an array of validators for a named context
32
+ #
33
+ # @param [String]
34
+ # Context name for which to return validators
35
+ # @return [Array<DataMapper::Validations::GenericValidator>]
36
+ # An array of validators bound to the given context
37
+ def context(name)
38
+ contexts[name] ||= OrderedSet.new
39
+ end
40
+
41
+ # Return an array of validators for a named property
42
+ #
43
+ # @param [Symbol]
44
+ # Property name for which to return validators
45
+ # @return [Array<DataMapper::Validations::GenericValidator>]
46
+ # An array of validators bound to the given property
47
+ def attribute(name)
48
+ attributes[name] ||= OrderedSet.new
49
+ end
50
+
51
+ # Clear all named context validators off of the resource
52
+ #
53
+ def clear!
54
+ contexts.clear
55
+ attributes.clear
56
+ end
57
+
58
+ # Create a new validator of the given klazz and push it onto the
59
+ # requested context for each of the attributes in +attributes+
60
+ #
61
+ # @param [DataMapper::Validations::GenericValidator] validator_class
62
+ # Validator class, example: DataMapper::Validations::LengthValidator
63
+ #
64
+ # @param [Array<Symbol>] attributes
65
+ # Attribute names given to validation macro, example:
66
+ # [:first_name, :last_name] in validates_presence_of :first_name, :last_name
67
+ #
68
+ # @param [Hash] options
69
+ # Options supplied to validation macro, example:
70
+ # {:context=>:default, :maximum=>50, :allow_nil=>true, :message=>nil}
71
+ #
72
+ # @option [Symbol] :context
73
+ # the context in which the new validator should be run
74
+ # @option [Boolean] :allow_nil
75
+ # whether or not the new validator should allow nil values
76
+ # @option [Boolean] :message
77
+ # the error message the new validator will provide on validation failure
78
+ def add(validator_class, *attributes)
79
+ options = attributes.last.kind_of?(Hash) ? attributes.pop.dup : {}
80
+ normalize_options(options)
81
+ validator_options = DataMapper::Ext::Hash.except(options, :context)
82
+
83
+ attributes.each do |attribute|
84
+ # TODO: is :context part of the Validator state (ie, intrinsic),
85
+ # or is it just membership in a collection?
86
+ validator = validator_class.new(attribute, validator_options)
87
+ attribute_validators = self.attribute(attribute)
88
+ attribute_validators << validator unless attribute_validators.include?(validator)
89
+
90
+ options[:context].each do |context|
91
+ context_validators = self.context(context)
92
+ next if context_validators.include?(validator)
93
+ context_validators << validator
94
+ # TODO: eliminate this, then eliminate the @model ivar entirely
95
+ Validations::ClassMethods.create_context_instance_methods(@model, context) if @model
96
+ end
97
+ end
98
+ end
99
+
100
+ # Clean up the argument list and return a opts hash, including the
101
+ # merging of any default opts. Set the context to default if none is
102
+ # provided. Also allow :context to be aliased to :on, :when & :group
103
+ #
104
+ # @param [Hash] options
105
+ # the options to be normalized
106
+ # @param [NilClass, Hash] defaults
107
+ # default keys/values to set on normalized options
108
+ #
109
+ # @return [Hash]
110
+ # the normalized options
111
+ #
112
+ # @api private
113
+ def normalize_options(options, defaults = nil)
114
+ context = [
115
+ options.delete(:group),
116
+ options.delete(:on),
117
+ options.delete(:when),
118
+ options.delete(:context)
119
+ ].compact.first
120
+
121
+ options[:context] = Array(context || :default)
122
+ options.update(defaults) unless defaults.nil?
123
+ options
124
+ end
125
+
126
+ # Returns the current validation context on the stack if valid for this model,
127
+ # nil if no contexts are defined for the model (and no contexts are on
128
+ # the validation stack), or :default if the current context is invalid for
129
+ # this model or no contexts have been defined for this model and
130
+ # no context is on the stack.
131
+ #
132
+ # @return [Symbol]
133
+ # the current validation context from the stack (if valid for this model),
134
+ # nil if no context is on the stack and no contexts are defined for this model,
135
+ # or :default if the context on the stack is invalid for this model or
136
+ # no context is on the stack and this model has at least one validation context
137
+ #
138
+ # @api private
139
+ #
140
+ # TODO: simplify the semantics of #current_context, #valid?
141
+ def current_context
142
+ context = Validations::Context.current
143
+ valid_context?(context) ? context : :default
144
+ end
145
+
146
+ # Test if the context is valid for the model
147
+ #
148
+ # @param [Symbol] context
149
+ # the context to test
150
+ #
151
+ # @return [Boolean]
152
+ # true if the context is valid for the model
153
+ #
154
+ # @api private
155
+ #
156
+ # TODO: investigate removing the `contexts.empty?` test here.
157
+ def valid_context?(context)
158
+ contexts.empty? || contexts.include?(context)
159
+ end
160
+
161
+ # Assert that the given context is valid for this model
162
+ #
163
+ # @param [Symbol] context
164
+ # the context to test
165
+ #
166
+ # @raise [InvalidContextError]
167
+ # raised if the context is not valid for this model
168
+ #
169
+ # @api private
170
+ #
171
+ # TODO: is this method actually needed?
172
+ def assert_valid(context)
173
+ unless valid_context?(context)
174
+ raise InvalidContextError, "#{context} is an invalid context, known contexts are #{contexts.keys.inspect}"
175
+ end
176
+ end
177
+
178
+ # Execute all validators in the named context against the target.
179
+ # Load together any properties that are designated lazy but are not
180
+ # yet loaded.
181
+ #
182
+ # @param [Symbol] named_context
183
+ # the context we are validating against
184
+ # @param [Object] target
185
+ # the resource that we are validating
186
+ # @return [Boolean]
187
+ # true if all are valid, otherwise false
188
+ def execute(named_context, target)
189
+ target.errors.clear!
190
+
191
+ available_validators = context(named_context)
192
+ executable_validators = available_validators.select { |v| v.execute?(target) }
193
+
194
+ # In the case of a new Resource or regular ruby class instance,
195
+ # everything needs to be validated completely, and no eager-loading
196
+ # logic should apply.
197
+ #
198
+ if target.kind_of?(DataMapper::Resource) && !target.new?
199
+ load_validated_properties(target, executable_validators)
200
+ end
201
+ executable_validators.map { |validator| validator.call(target) }.all?
202
+ end
203
+
204
+ # Load all lazy, not-yet-loaded properties that need validation,
205
+ # all at once.
206
+ def load_validated_properties(resource, validators)
207
+ properties = resource.model.properties
208
+
209
+ properties_to_load = validators.map { |validator|
210
+ properties[validator.field_name]
211
+ }.compact.select { |property|
212
+ property.lazy? && !property.loaded?(resource)
213
+ }
214
+
215
+ resource.__send__(:eager_load, properties_to_load)
216
+ end
217
+
218
+ end # module ContextualValidators
219
+ end # module Validations
220
+ end # module DataMapper