ardm-validations 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +35 -0
- data/.travis.yml +11 -0
- data/Gemfile +51 -0
- data/LICENSE +21 -0
- data/README.rdoc +122 -0
- data/Rakefile +4 -0
- data/ardm-validations.gemspec +28 -0
- data/lib/ardm-validations.rb +1 -0
- data/lib/dm-validations.rb +169 -0
- data/lib/dm-validations/auto_validate.rb +252 -0
- data/lib/dm-validations/context.rb +66 -0
- data/lib/dm-validations/contextual_validators.rb +220 -0
- data/lib/dm-validations/exceptions.rb +5 -0
- data/lib/dm-validations/formats/email.rb +65 -0
- data/lib/dm-validations/formats/url.rb +27 -0
- data/lib/dm-validations/support/object.rb +18 -0
- data/lib/dm-validations/support/ordered_hash.rb +434 -0
- data/lib/dm-validations/validation_errors.rb +137 -0
- data/lib/dm-validations/validators/absent_field_validator.rb +58 -0
- data/lib/dm-validations/validators/acceptance_validator.rb +79 -0
- data/lib/dm-validations/validators/block_validator.rb +61 -0
- data/lib/dm-validations/validators/confirmation_validator.rb +92 -0
- data/lib/dm-validations/validators/format_validator.rb +124 -0
- data/lib/dm-validations/validators/generic_validator.rb +184 -0
- data/lib/dm-validations/validators/length_validator.rb +249 -0
- data/lib/dm-validations/validators/method_validator.rb +64 -0
- data/lib/dm-validations/validators/numeric_validator.rb +182 -0
- data/lib/dm-validations/validators/primitive_validator.rb +58 -0
- data/lib/dm-validations/validators/required_field_validator.rb +83 -0
- data/lib/dm-validations/validators/uniqueness_validator.rb +67 -0
- data/lib/dm-validations/validators/within_validator.rb +74 -0
- data/lib/dm-validations/version.rb +5 -0
- data/spec/fixtures/barcode.rb +40 -0
- data/spec/fixtures/basketball_court.rb +58 -0
- data/spec/fixtures/basketball_player.rb +34 -0
- data/spec/fixtures/beta_tester_account.rb +33 -0
- data/spec/fixtures/bill_of_landing.rb +47 -0
- data/spec/fixtures/boat_dock.rb +26 -0
- data/spec/fixtures/city.rb +24 -0
- data/spec/fixtures/company.rb +93 -0
- data/spec/fixtures/corporate_world.rb +39 -0
- data/spec/fixtures/country.rb +24 -0
- data/spec/fixtures/ethernet_frame.rb +56 -0
- data/spec/fixtures/event.rb +44 -0
- data/spec/fixtures/g3_concert.rb +57 -0
- data/spec/fixtures/jabberwock.rb +27 -0
- data/spec/fixtures/kayak.rb +28 -0
- data/spec/fixtures/lernean_hydra.rb +39 -0
- data/spec/fixtures/llama_spaceship.rb +15 -0
- data/spec/fixtures/mathematical_function.rb +34 -0
- data/spec/fixtures/memory_object.rb +30 -0
- data/spec/fixtures/mittelschnauzer.rb +39 -0
- data/spec/fixtures/motor_launch.rb +21 -0
- data/spec/fixtures/multibyte.rb +16 -0
- data/spec/fixtures/page.rb +32 -0
- data/spec/fixtures/phone_number.rb +28 -0
- data/spec/fixtures/pirogue.rb +28 -0
- data/spec/fixtures/programming_language.rb +83 -0
- data/spec/fixtures/reservation.rb +38 -0
- data/spec/fixtures/scm_operation.rb +56 -0
- data/spec/fixtures/sms_message.rb +22 -0
- data/spec/fixtures/udp_packet.rb +49 -0
- data/spec/integration/absent_field_validator/absent_field_validator_spec.rb +90 -0
- data/spec/integration/absent_field_validator/spec_helper.rb +7 -0
- data/spec/integration/acceptance_validator/acceptance_validator_spec.rb +196 -0
- data/spec/integration/acceptance_validator/spec_helper.rb +7 -0
- data/spec/integration/automatic_validation/custom_messages_for_inferred_validation_spec.rb +57 -0
- data/spec/integration/automatic_validation/disabling_inferred_validation_spec.rb +49 -0
- data/spec/integration/automatic_validation/inferred_boolean_properties_validation_spec.rb +100 -0
- data/spec/integration/automatic_validation/inferred_float_property_validation_spec.rb +45 -0
- data/spec/integration/automatic_validation/inferred_format_validation_spec.rb +35 -0
- data/spec/integration/automatic_validation/inferred_integer_properties_validation_spec.rb +70 -0
- data/spec/integration/automatic_validation/inferred_length_validation_spec.rb +142 -0
- data/spec/integration/automatic_validation/inferred_presence_validation_spec.rb +45 -0
- data/spec/integration/automatic_validation/inferred_primitive_validation_spec.rb +22 -0
- data/spec/integration/automatic_validation/inferred_uniqueness_validation_spec.rb +52 -0
- data/spec/integration/automatic_validation/inferred_within_validation_spec.rb +39 -0
- data/spec/integration/automatic_validation/spec_helper.rb +57 -0
- data/spec/integration/block_validator/block_validator_spec.rb +32 -0
- data/spec/integration/block_validator/spec_helper.rb +5 -0
- data/spec/integration/conditional_validation/if_condition_spec.rb +63 -0
- data/spec/integration/conditional_validation/spec_helper.rb +5 -0
- data/spec/integration/confirmation_validator/confirmation_validator_spec.rb +76 -0
- data/spec/integration/confirmation_validator/spec_helper.rb +5 -0
- data/spec/integration/datamapper_models/association_validation_spec.rb +29 -0
- data/spec/integration/datamapper_models/inheritance_spec.rb +82 -0
- data/spec/integration/dirty_attributes/dirty_attributes_spec.rb +13 -0
- data/spec/integration/duplicated_validations/duplicated_validations_spec.rb +24 -0
- data/spec/integration/duplicated_validations/spec_helper.rb +5 -0
- data/spec/integration/format_validator/email_format_validator_spec.rb +139 -0
- data/spec/integration/format_validator/format_validator_spec.rb +64 -0
- data/spec/integration/format_validator/regexp_validator_spec.rb +33 -0
- data/spec/integration/format_validator/spec_helper.rb +5 -0
- data/spec/integration/format_validator/url_format_validator_spec.rb +93 -0
- data/spec/integration/length_validator/default_value_spec.rb +14 -0
- data/spec/integration/length_validator/equality_spec.rb +87 -0
- data/spec/integration/length_validator/error_message_spec.rb +22 -0
- data/spec/integration/length_validator/maximum_spec.rb +49 -0
- data/spec/integration/length_validator/minimum_spec.rb +54 -0
- data/spec/integration/length_validator/range_spec.rb +87 -0
- data/spec/integration/length_validator/spec_helper.rb +7 -0
- data/spec/integration/method_validator/method_validator_spec.rb +241 -0
- data/spec/integration/method_validator/spec_helper.rb +5 -0
- data/spec/integration/numeric_validator/equality_with_float_type_spec.rb +65 -0
- data/spec/integration/numeric_validator/equality_with_integer_type_spec.rb +41 -0
- data/spec/integration/numeric_validator/float_type_spec.rb +90 -0
- data/spec/integration/numeric_validator/gt_with_float_type_spec.rb +37 -0
- data/spec/integration/numeric_validator/gte_with_float_type_spec.rb +37 -0
- data/spec/integration/numeric_validator/integer_only_true_spec.rb +91 -0
- data/spec/integration/numeric_validator/integer_type_spec.rb +86 -0
- data/spec/integration/numeric_validator/lt_with_float_type_spec.rb +37 -0
- data/spec/integration/numeric_validator/lte_with_float_type_spec.rb +37 -0
- data/spec/integration/numeric_validator/spec_helper.rb +5 -0
- data/spec/integration/primitive_validator/primitive_validator_spec.rb +92 -0
- data/spec/integration/primitive_validator/spec_helper.rb +5 -0
- data/spec/integration/pure_ruby_objects/plain_old_ruby_object_validation_spec.rb +118 -0
- data/spec/integration/required_field_validator/association_spec.rb +72 -0
- data/spec/integration/required_field_validator/boolean_type_value_spec.rb +155 -0
- data/spec/integration/required_field_validator/date_type_value_spec.rb +127 -0
- data/spec/integration/required_field_validator/datetime_type_value_spec.rb +127 -0
- data/spec/integration/required_field_validator/float_type_value_spec.rb +131 -0
- data/spec/integration/required_field_validator/integer_type_value_spec.rb +99 -0
- data/spec/integration/required_field_validator/plain_old_ruby_object_spec.rb +35 -0
- data/spec/integration/required_field_validator/shared_examples.rb +26 -0
- data/spec/integration/required_field_validator/spec_helper.rb +7 -0
- data/spec/integration/required_field_validator/string_type_value_spec.rb +167 -0
- data/spec/integration/required_field_validator/text_type_value_spec.rb +49 -0
- data/spec/integration/shared/default_validation_context.rb +13 -0
- data/spec/integration/shared/valid_and_invalid_model.rb +35 -0
- data/spec/integration/uniqueness_validator/spec_helper.rb +5 -0
- data/spec/integration/uniqueness_validator/uniqueness_validator_spec.rb +116 -0
- data/spec/integration/within_validator/spec_helper.rb +5 -0
- data/spec/integration/within_validator/within_validator_spec.rb +168 -0
- data/spec/public/resource_spec.rb +105 -0
- data/spec/rcov.opts +6 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/unit/contextual_validators/emptiness_spec.rb +50 -0
- data/spec/unit/contextual_validators/execution_spec.rb +48 -0
- data/spec/unit/contextual_validators/spec_helper.rb +37 -0
- data/spec/unit/generic_validator/equality_operator_spec.rb +26 -0
- data/spec/unit/generic_validator/optional_spec.rb +54 -0
- data/spec/unit/validation_errors/adding_spec.rb +54 -0
- data/spec/unit/validation_errors/emptiness_spec.rb +38 -0
- data/spec/unit/validation_errors/enumerable_spec.rb +32 -0
- data/spec/unit/validation_errors/reading_spec.rb +35 -0
- data/spec/unit/validation_errors/respond_to_spec.rb +15 -0
- data/spec/unit/validators/within_validator_spec.rb +23 -0
- data/tasks/spec.rake +38 -0
- data/tasks/yard.rake +9 -0
- data/tasks/yardstick.rake +19 -0
- 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
|