dm-validations 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. data/LICENSE +20 -0
  2. data/README +72 -0
  3. data/Rakefile +74 -0
  4. data/TODO +16 -0
  5. data/lib/dm-validations.rb +208 -0
  6. data/lib/dm-validations/absent_field_validator.rb +60 -0
  7. data/lib/dm-validations/acceptance_validator.rb +76 -0
  8. data/lib/dm-validations/auto_validate.rb +98 -0
  9. data/lib/dm-validations/confirmation_validator.rb +76 -0
  10. data/lib/dm-validations/contextual_validators.rb +56 -0
  11. data/lib/dm-validations/custom_validator.rb +72 -0
  12. data/lib/dm-validations/format_validator.rb +94 -0
  13. data/lib/dm-validations/formats/email.rb +40 -0
  14. data/lib/dm-validations/generic_validator.rb +92 -0
  15. data/lib/dm-validations/length_validator.rb +113 -0
  16. data/lib/dm-validations/method_validator.rb +58 -0
  17. data/lib/dm-validations/numeric_validator.rb +66 -0
  18. data/lib/dm-validations/primitive_validator.rb +60 -0
  19. data/lib/dm-validations/required_field_validator.rb +88 -0
  20. data/lib/dm-validations/support/object.rb +5 -0
  21. data/lib/dm-validations/uniqueness_validator.rb +61 -0
  22. data/lib/dm-validations/validation_errors.rb +63 -0
  23. data/lib/dm-validations/within_validator.rb +41 -0
  24. data/spec/integration/absent_field_validator_spec.rb +34 -0
  25. data/spec/integration/acceptance_validator_spec.rb +87 -0
  26. data/spec/integration/auto_validate_spec.rb +262 -0
  27. data/spec/integration/confirmation_validator_spec.rb +66 -0
  28. data/spec/integration/contextual_validators_spec.rb +28 -0
  29. data/spec/integration/custom_validator_spec.rb +9 -0
  30. data/spec/integration/format_validator_spec.rb +118 -0
  31. data/spec/integration/generic_validator_spec.rb +9 -0
  32. data/spec/integration/length_validator_spec.rb +113 -0
  33. data/spec/integration/method_validator_spec.rb +31 -0
  34. data/spec/integration/numeric_validator_spec.rb +192 -0
  35. data/spec/integration/primitive_validator_spec.rb +25 -0
  36. data/spec/integration/required_field_validator_spec.rb +93 -0
  37. data/spec/integration/uniqueness_validator_spec.rb +81 -0
  38. data/spec/integration/validation_errors_spec.rb +18 -0
  39. data/spec/integration/validation_spec.rb +339 -0
  40. data/spec/integration/within_validator_spec.rb +35 -0
  41. data/spec/spec.opts +2 -0
  42. data/spec/spec_helper.rb +26 -0
  43. metadata +104 -0
@@ -0,0 +1,5 @@
1
+ class Object
2
+ def validatable?
3
+ false
4
+ end
5
+ end
@@ -0,0 +1,61 @@
1
+ module DataMapper
2
+ module Validate
3
+
4
+ ##
5
+ #
6
+ # @author Guy van den Berg
7
+ # @since 0.9
8
+ class UniquenessValidator < GenericValidator
9
+ include Assertions
10
+
11
+ def initialize(field_name, options = {})
12
+ assert_kind_of 'scope', options[:scope], Array, Symbol if options.has_key?(:scope)
13
+ super
14
+ @field_name, @options = field_name, options
15
+ end
16
+
17
+ def call(target)
18
+ scope = Array(@options[:scope])
19
+
20
+ return true if @options[:allow_nil] && target.send(field_name).nil?
21
+
22
+ repository_name = target.repository.name
23
+
24
+ opts = { field_name => target.validation_property_value(field_name) }
25
+
26
+ scope.each do |item|
27
+ if !target.model.properties(repository_name)[item].nil?
28
+ opts[item] = target.validation_property_value(item)
29
+ elsif target.model.relationships(repository_name).has_key?(item)
30
+ target.validation_association_keys(item).each do |key|
31
+ opts[key] = target.validation_property_value(key)
32
+ end
33
+ end
34
+ end
35
+
36
+ resource = repository(repository_name) { target.model.first(opts) }
37
+
38
+ return true if resource.nil?
39
+
40
+ # is target and found resource identic? same instance... but not ==
41
+ return true if resource.repository.name == repository_name && resource.model == target.model && resource.key == target.key
42
+
43
+ error_message = @options[:message] || "%s is already taken".t(Extlib::Inflection.humanize(field_name))
44
+ add_error(target, error_message , field_name)
45
+
46
+ return false
47
+ end
48
+ end # class UniquenessValidator
49
+
50
+ module ValidatesIsUnique
51
+
52
+ # Validate the uniqueness of a field
53
+ #
54
+ def validates_is_unique(*fields)
55
+ opts = opts_from_validator_args(fields)
56
+ add_validator_to_context(opts, fields, DataMapper::Validate::UniquenessValidator)
57
+ end
58
+
59
+ end # module ValidatesIsUnique
60
+ end # module Validate
61
+ end # module DataMapper
@@ -0,0 +1,63 @@
1
+ module DataMapper
2
+ module Validate
3
+
4
+ ##
5
+ #
6
+ # @author Guy van den Berg
7
+ # @since 0.9
8
+ class ValidationErrors
9
+
10
+ include Enumerable
11
+
12
+ # Clear existing validation errors.
13
+ def clear!
14
+ errors.clear
15
+ end
16
+
17
+ # Add a validation error. Use the field_name :general if the errors does
18
+ # not apply to a specific field of the Resource.
19
+ #
20
+ # @param <Symbol> field_name the name of the field that caused the error
21
+ # @param <String> message the message to add
22
+ def add(field_name, message)
23
+ (errors[field_name] ||= []) << message
24
+ end
25
+
26
+ # Collect all errors into a single list.
27
+ def full_messages
28
+ errors.inject([]) do |list,pair|
29
+ list += pair.last
30
+ end
31
+ end
32
+
33
+ # Return validation errors for a particular field_name.
34
+ #
35
+ # @param <Symbol> field_name the name of the field you want an error for
36
+ def on(field_name)
37
+ errors_for_field = errors[field_name]
38
+ errors_for_field.blank? ? nil : errors_for_field
39
+ end
40
+
41
+ def each
42
+ errors.map.each do |k,v|
43
+ next if v.blank?
44
+ yield(v)
45
+ end
46
+ end
47
+
48
+ def empty?
49
+ entries.empty?
50
+ end
51
+
52
+ def method_missing(meth, *args, &block)
53
+ errors.send(meth, *args, &block)
54
+ end
55
+
56
+ private
57
+ def errors
58
+ @errors ||= {}
59
+ end
60
+
61
+ end # class ValidationErrors
62
+ end # module Validate
63
+ end # module DataMapper
@@ -0,0 +1,41 @@
1
+ module DataMapper
2
+ module Validate
3
+
4
+ ##
5
+ #
6
+ # @author Guy van den Berg
7
+ # @since 0.9
8
+ class WithinValidator < GenericValidator
9
+
10
+ def initialize(field_name, options={})
11
+ super
12
+ @field_name, @options = field_name, options
13
+ @options[:set] = [] unless @options.has_key?(:set)
14
+ end
15
+
16
+ def call(target)
17
+ includes = @options[:set].include?(target.send(field_name))
18
+ return true if includes
19
+
20
+ s = ''
21
+ @options[:set].each {|item| s = s + "#{item}, "}
22
+ s = '[' + s[0..(s.length-3)] + ']'
23
+
24
+ error_message = @options[:message] || "%s must be one of #{s}".t(Extlib::Inflection.humanize(@field_name))
25
+ add_error(target, error_message , @field_name)
26
+ return false
27
+ end
28
+ end # class WithinValidator
29
+
30
+ module ValidatesWithin
31
+
32
+ # Validate the absence of a field
33
+ #
34
+ def validates_within(*fields)
35
+ opts = opts_from_validator_args(fields)
36
+ add_validator_to_context(opts, fields, DataMapper::Validate::WithinValidator)
37
+ end
38
+
39
+ end # module ValidatesWithin
40
+ end # module Validate
41
+ end # module DataMapper
@@ -0,0 +1,34 @@
1
+ require 'pathname'
2
+ require Pathname(__FILE__).dirname.expand_path.parent + 'spec_helper'
3
+
4
+ describe DataMapper::Validate::AbsentFieldValidator do
5
+ before(:all) do
6
+ class Kayak
7
+ include DataMapper::Resource
8
+ property :id, Integer, :key => true
9
+ property :salesman, String, :auto_validation => false
10
+ validates_absent :salesman, :when => :sold
11
+ end
12
+
13
+ class Pirogue
14
+ include DataMapper::Resource
15
+ property :id, Integer, :key => true
16
+ property :salesman, String, :default => 'Layfayette'
17
+ validates_absent :salesman, :when => :sold
18
+ end
19
+ end
20
+
21
+ it "should validate the absence of a value on an instance of a resource" do
22
+ kayak = Kayak.new
23
+ kayak.valid_for_sold?.should == true
24
+
25
+ kayak.salesman = 'Joe'
26
+ kayak.valid_for_sold?.should_not == true
27
+ end
28
+
29
+ it "should validate the absence of a value and ensure defaults" do
30
+ pirogue = Pirogue.new
31
+ pirogue.should_not be_valid_for_sold
32
+ end
33
+
34
+ end
@@ -0,0 +1,87 @@
1
+ require 'pathname'
2
+ require Pathname(__FILE__).dirname.expand_path.parent + 'spec_helper'
3
+
4
+ describe DataMapper::Validate::AcceptanceValidator do
5
+ describe "with standard options" do
6
+ before(:all) do
7
+ class SkimBat
8
+ include DataMapper::Resource
9
+ property :id, Integer, :serial => true
10
+ property :sailyness, Boolean
11
+ validates_is_accepted :sailyness
12
+ end
13
+ @s = SkimBat.new
14
+ end
15
+
16
+ it "should validate if a resource instance has been accepted" do
17
+ @s.sailyness = "1"
18
+ @s.valid?.should == true
19
+ end
20
+
21
+ it "should not validate if a resource instance has not been accepted" do
22
+ @s.sailyness = "0"
23
+ @s.valid?.should == false
24
+ end
25
+
26
+ it "should allow nil acceptance" do
27
+ @s.sailyness = nil
28
+ @s.valid?.should == true
29
+ end
30
+
31
+ it "should add the default message when invalid" do
32
+ @s.sailyness = "0"
33
+ @s.valid?.should == false
34
+ @s.errors.full_messages.join(" ").should =~ /#{DataMapper::Validate::AcceptanceValidator.default_message_for_field("sailyness")}/
35
+ end
36
+ end
37
+ describe "with :allow_nil => false" do
38
+ before(:all) do
39
+ SkimBat.class_eval do
40
+ validators.clear!
41
+ validates_is_accepted :sailyness, :allow_nil => false
42
+ end
43
+ @s = SkimBat.new
44
+ end
45
+
46
+ it "should not allow nil acceptance" do
47
+ @s.sailyness = nil
48
+ @s.valid?.should == false
49
+ end
50
+ end
51
+
52
+ describe "with custom :accept" do
53
+ before(:all) do
54
+ SkimBat.class_eval do
55
+ validators.clear!
56
+ validates_is_accepted :sailyness, :accept => "true"
57
+ end
58
+ @s = SkimBat.new
59
+ end
60
+
61
+ it "should validate if a resource instance has been accepted" do
62
+ @s.sailyness = "true"
63
+ @s.valid?.should == true
64
+ end
65
+
66
+ it "should not validate if a resource instance has not been accepted" do
67
+ @s.sailyness = "false"
68
+ @s.valid?.should == false
69
+ end
70
+ end
71
+
72
+ describe "with custom message" do
73
+ before(:all) do
74
+ SkimBat.class_eval do
75
+ validators.clear!
76
+ validates_is_accepted :sailyness, :message => "hehu!"
77
+ end
78
+ @s = SkimBat.new
79
+ end
80
+
81
+ it "should append the custom message when invalid" do
82
+ @s.sailyness = "0"
83
+ @s.valid?.should == false
84
+ @s.errors.full_messages.join(" ").should =~ /hehu!/
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,262 @@
1
+ require 'pathname'
2
+ require Pathname(__FILE__).dirname.expand_path.parent + 'spec_helper'
3
+
4
+ module TypecastBypassSetter
5
+ # Bypass typecasting so we can set values for specs
6
+ def set(attributes)
7
+ attributes.each do |k,v|
8
+ instance_variable_set("@#{k}", v)
9
+ end
10
+ end
11
+ end
12
+
13
+ class SailBoat
14
+ include DataMapper::Resource
15
+ property :id, Integer, :key => true
16
+ property :name, String, :nullable => false, :validates => :presence_test
17
+ property :description, String, :length => 10, :validates => :length_test_1
18
+ property :notes, String, :length => 2..10, :validates => :length_test_2
19
+ property :no_validation, String, :auto_validation => false
20
+ property :salesman, String, :nullable => false, :validates => [:multi_context_1, :multi_context_2]
21
+ property :code, String, :format => Proc.new { |code| code =~ /A\d{4}\z/ }, :validates => :format_test
22
+ property :allow_nil, String, :size => 5..10, :nullable => true, :validates => :nil_test
23
+ property :build_date, Date, :validates => :primitive_test
24
+ property :float, Float, :precision => 2, :scale => 1
25
+ property :big_decimal, BigDecimal, :precision => 2, :scale => 1
26
+
27
+ include TypecastBypassSetter
28
+ end
29
+
30
+ class HasNullableBoolean
31
+ include DataMapper::Resource
32
+ property :id, Integer, :key => true
33
+ property :bool, Boolean # :nullable => true by default
34
+
35
+ include TypecastBypassSetter
36
+ end
37
+
38
+ class HasNotNullableBoolean
39
+ include DataMapper::Resource
40
+ property :id, Integer, :key => true
41
+ property :bool, Boolean, :nullable => false
42
+
43
+ include TypecastBypassSetter
44
+ end
45
+
46
+ class HasNotNullableParanoidBoolean
47
+ include DataMapper::Resource
48
+ property :id, Integer, :key => true
49
+ property :bool, ParanoidBoolean, :nullable => false
50
+
51
+ include TypecastBypassSetter
52
+ end
53
+
54
+ describe "Automatic Validation from Property Definition" do
55
+ it "should have a hook for adding auto validations called from
56
+ DataMapper::Property#new" do
57
+ SailBoat.should respond_to(:auto_generate_validations)
58
+ end
59
+
60
+ it "should auto add a validates_is_present when property has option
61
+ :nullable => false" do
62
+ validator = SailBoat.validators.context(:presence_test).first
63
+ validator.should be_kind_of(DataMapper::Validate::RequiredFieldValidator)
64
+ validator.field_name.should == :name
65
+
66
+ boat = SailBoat.new
67
+ boat.valid_for_presence_test?.should == false
68
+ boat.name = 'Float'
69
+ boat.valid_for_presence_test?.should == true
70
+ end
71
+
72
+ it "should auto add a validates_length for maximum size on String properties" do
73
+ # max length test max=10
74
+ boat = SailBoat.new
75
+ boat.valid_for_length_test_1?.should == true #no minimum length
76
+ boat.description = 'ABCDEFGHIJK' #11
77
+ boat.valid_for_length_test_1?.should == false
78
+ boat.description = 'ABCDEFGHIJ' #10
79
+ boat.valid_for_length_test_1?.should == true
80
+ end
81
+
82
+ it "should auto add validates_length within a range when option :length
83
+ or :size is a range" do
84
+ # Range test notes = 2..10
85
+ boat = SailBoat.new
86
+ boat.should be_valid_for_length_test_2
87
+ boat.notes = 'AB' #2
88
+ boat.should be_valid_for_length_test_2
89
+ boat.notes = 'ABCDEFGHIJK' #11
90
+ boat.should_not be_valid_for_length_test_2
91
+ boat.notes = 'ABCDEFGHIJ' #10
92
+ boat.should be_valid_for_length_test_2
93
+ end
94
+
95
+ it "should auto add a validates_format if the :format option is given" do
96
+ # format test - format = /A\d{4}\z/ on code
97
+ boat = SailBoat.new
98
+ boat.should be_valid_for_format_test
99
+ boat.code = 'A1234'
100
+ boat.should be_valid_for_format_test
101
+ boat.code = 'BAD CODE'
102
+ boat.should_not be_valid_for_format_test
103
+ end
104
+
105
+ it "should auto validate all strings for max length" do
106
+ klass = Class.new do
107
+ include DataMapper::Resource
108
+ property :id, Integer, :serial => true
109
+ property :name, String
110
+ end
111
+ t = klass.new(:id => 1)
112
+ t.should be_valid
113
+ t.name = 'Lip­smackin­thirst­quenchin­acetastin­motivatin­good­buzzin­cool­talkin­high­walkin­fast­livin­ever­givin­cool­fizzin'
114
+ t.should_not be_valid
115
+ t.errors.full_messages.should include('Name must be less than 50 characters long')
116
+ end
117
+
118
+ it "should auto validate the primitive type" do
119
+ validator = SailBoat.validators.context(:primitive_test).first
120
+ validator.should be_kind_of(DataMapper::Validate::PrimitiveValidator)
121
+ boat = SailBoat.new
122
+ boat.should be_valid_for_primitive_test
123
+ boat.build_date = 'ABC'
124
+ boat.should_not be_valid_for_primitive_test
125
+ end
126
+
127
+ it "should not auto add any validators if the option :auto_validation => false was given" do
128
+ klass = Class.new do
129
+ include DataMapper::Resource
130
+ property :id, Integer, :serial => true, :auto_validation => false
131
+ property :name, String, :nullable => false, :auto_validation => false
132
+ property :bool, DM::Boolean, :nullable => false, :auto_validation => false
133
+ end
134
+ klass.new.valid?.should == true
135
+ end
136
+
137
+ it "should auto add range checking the length of a string while still allowing null values" do
138
+ boat = SailBoat.new
139
+ boat.allow_nil = 'ABC'
140
+ boat.should_not be_valid_for_nil_test
141
+ boat.errors.on(:allow_nil).should include('Allow nil must be between 5 and 10 characters long')
142
+
143
+ boat.allow_nil = 'ABCDEFG'
144
+ boat.should be_valid_for_nil_test
145
+
146
+ boat.allow_nil = 'ABCDEFGHIJKLMNOP'
147
+ boat.should_not be_valid_for_nil_test
148
+ boat.errors.on(:allow_nil).should include('Allow nil must be between 5 and 10 characters long')
149
+
150
+ boat.allow_nil = nil
151
+ boat.should be_valid_for_nil_test
152
+ end
153
+
154
+ describe 'for Integer properties' do
155
+ before do
156
+ @boat = SailBoat.new
157
+ end
158
+
159
+ it 'should allow integers' do
160
+ @boat.set(:id => 1)
161
+ @boat.should be_valid
162
+ end
163
+
164
+ it 'should not allow floats' do
165
+ @boat.set(:id => 1.0)
166
+ @boat.should_not be_valid
167
+ @boat.errors.on(:id).should == [ 'Id must be an integer' ]
168
+ end
169
+
170
+ it 'should not allow decimals' do
171
+ @boat.set(:id => BigDecimal('1'))
172
+ @boat.should_not be_valid
173
+ @boat.errors.on(:id).should == [ 'Id must be an integer' ]
174
+ end
175
+ end
176
+
177
+ describe 'for nullable Boolean properties' do
178
+ before do
179
+ @boat = HasNullableBoolean.new(:id => 1)
180
+ end
181
+
182
+ it 'should allow true' do
183
+ @boat.set(:bool => true)
184
+ @boat.should be_valid
185
+ end
186
+
187
+ it 'should allow false' do
188
+ @boat.set(:bool => false)
189
+ @boat.should be_valid
190
+ end
191
+
192
+ it 'should allow nil' do
193
+ @boat.set(:bool => nil)
194
+ @boat.should be_valid
195
+ end
196
+ end
197
+
198
+ describe 'for non-nullable Boolean properties' do
199
+ before do
200
+ @boat = HasNotNullableBoolean.new(:id => 1)
201
+ end
202
+
203
+ it 'should allow true' do
204
+ @boat.set(:bool => true)
205
+ @boat.should be_valid
206
+ end
207
+
208
+ it 'should allow false' do
209
+ @boat.set(:bool => false)
210
+ @boat.should be_valid
211
+ end
212
+
213
+ it 'should not allow nil' do
214
+ @boat.set(:bool => nil)
215
+ @boat.should_not be_valid
216
+ end
217
+ end
218
+
219
+ describe 'for non-nullable ParanoidBoolean properties' do
220
+ before do
221
+ @boat = HasNotNullableParanoidBoolean.new(:id => 1)
222
+ end
223
+
224
+ it 'should allow true' do
225
+ @boat.set(:bool => true)
226
+ @boat.should be_valid
227
+ end
228
+
229
+ it 'should allow false' do
230
+ @boat.set(:bool => false)
231
+ @boat.should be_valid
232
+ end
233
+
234
+ it 'should not allow nil' do
235
+ @boat.set(:bool => nil)
236
+ @boat.should_not be_valid
237
+ end
238
+ end
239
+
240
+ { :float => Float, :big_decimal => BigDecimal }.each do |column,type|
241
+ describe "for #{type} properties" do
242
+ before do
243
+ @boat = SailBoat.new(:id => 1)
244
+ end
245
+
246
+ it 'should allow integers' do
247
+ @boat.set(column => 1)
248
+ @boat.should be_valid
249
+ end
250
+
251
+ it 'should allow floats' do
252
+ @boat.set(column => '1.0')
253
+ @boat.should be_valid
254
+ end
255
+
256
+ it 'should allow decimals' do
257
+ @boat.set(column => BigDecimal('1'))
258
+ @boat.should be_valid
259
+ end
260
+ end
261
+ end
262
+ end