ardm-validations 1.2.0
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.
- 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
|