dm-validations 1.0.2 → 1.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. data/Gemfile +25 -94
  2. data/LICENSE +1 -1
  3. data/Rakefile +2 -7
  4. data/VERSION +1 -1
  5. data/dm-validations.gemspec +281 -270
  6. data/lib/dm-validations.rb +36 -35
  7. data/lib/dm-validations/auto_validate.rb +10 -5
  8. data/lib/dm-validations/contextual_validators.rb +41 -4
  9. data/lib/dm-validations/formats/email.rb +12 -8
  10. data/lib/dm-validations/validation_errors.rb +4 -0
  11. data/lib/dm-validations/validators/acceptance_validator.rb +4 -0
  12. data/lib/dm-validations/validators/format_validator.rb +3 -3
  13. data/lib/dm-validations/validators/generic_validator.rb +8 -20
  14. data/lib/dm-validations/validators/length_validator.rb +7 -1
  15. data/lib/dm-validations/validators/uniqueness_validator.rb +8 -2
  16. data/spec/fixtures/mittelschnauzer.rb +1 -0
  17. data/spec/integration/automatic_validation/inferred_boolean_properties_validation_spec.rb +20 -24
  18. data/spec/integration/automatic_validation/inferred_float_property_validation_spec.rb +11 -3
  19. data/spec/integration/automatic_validation/inferred_integer_properties_validation_spec.rb +9 -14
  20. data/spec/integration/automatic_validation/inferred_length_validation_spec.rb +9 -0
  21. data/spec/integration/automatic_validation/inferred_uniqueness_validation_spec.rb +48 -0
  22. data/spec/integration/automatic_validation/spec_helper.rb +2 -19
  23. data/spec/integration/format_validator/email_format_validator_spec.rb +15 -1
  24. data/spec/unit/generic_validator/optional_spec.rb +54 -0
  25. data/spec/unit/validation_errors/respond_to_spec.rb +15 -0
  26. data/tasks/spec.rake +0 -3
  27. metadata +69 -42
  28. data/.gitignore +0 -37
  29. data/tasks/ci.rake +0 -1
  30. data/tasks/local_gemfile.rake +0 -16
  31. data/tasks/metrics.rake +0 -36
@@ -1,43 +1,48 @@
1
1
  require 'dm-core'
2
2
 
3
3
  begin
4
-
4
+ # We need array for extract_options! which attr_accessors uses, at least in AS
5
+ # 2.3.3.
6
+ require 'active_support/core_ext/array'
5
7
  require 'active_support/core_ext/class/attribute_accessors'
6
- require 'active_support/core_ext/object/blank'
7
- require 'active_support/ordered_hash'
8
-
9
- class Object
10
- # If receiver is callable, calls it and
11
- # returns result. If not, just returns receiver
12
- # itself
13
- #
14
- # @return [Object]
15
- def try_call(*args)
16
- if self.respond_to?(:call)
17
- self.call(*args)
18
- else
19
- self
20
- end
21
- end
22
- end
8
+ rescue LoadError
9
+ require 'extlib/class'
10
+ end
23
11
 
12
+ begin
13
+ require 'active_support/core_ext/object/blank'
24
14
  rescue LoadError
15
+ require 'extlib/blank'
16
+ end
25
17
 
26
- require 'extlib/class'
18
+ begin
19
+ require 'active_support/ordered_hash'
20
+ rescue LoadError
27
21
  require 'extlib/dictionary'
28
- require 'extlib/blank'
29
- require 'extlib/try_dup'
30
- require 'extlib/object'
31
22
 
32
- module ActiveSupport
33
- OrderedHash = Dictionary
23
+ module ::ActiveSupport
24
+ OrderedHash = ::Dictionary
34
25
  end
26
+ end
35
27
 
28
+ class Object
29
+ # If receiver is callable, calls it and
30
+ # returns result. If not, just returns receiver
31
+ # itself
32
+ #
33
+ # @return [Object]
34
+ def try_call(*args)
35
+ if self.respond_to?(:call)
36
+ self.call(*args)
37
+ else
38
+ self
39
+ end
40
+ end
36
41
  end
37
42
 
38
43
  module DataMapper
39
44
  class Property
40
- def self.new(model, name, options = {}, type = nil)
45
+ def self.new(model, name, options = {})
41
46
  property = super
42
47
  property.model.auto_generate_validations(property)
43
48
 
@@ -77,16 +82,6 @@ module DataMapper
77
82
  extend Chainable
78
83
 
79
84
  def self.included(model)
80
- model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
81
- def self.create(attributes = {}, *args)
82
- resource = new(attributes)
83
- resource.save(*args)
84
- resource
85
- end
86
- RUBY
87
-
88
- # models that are non DM resources must get .validators
89
- # and other methods, too
90
85
  model.extend ClassMethods
91
86
  end
92
87
 
@@ -181,6 +176,12 @@ module DataMapper
181
176
  end
182
177
  end
183
178
 
179
+ def create(attributes = {}, *args)
180
+ resource = new(attributes)
181
+ resource.save(*args)
182
+ resource
183
+ end
184
+
184
185
  private
185
186
 
186
187
  # Clean up the argument list and return a opts hash, including the
@@ -130,11 +130,16 @@ module DataMapper
130
130
  end
131
131
 
132
132
  def infer_length_validation_for(property, options)
133
- return unless [ DataMapper::Property::String, DataMapper::Property::Text ].include?(property.class)
133
+ return unless [ DataMapper::Property::String, DataMapper::Property::Text ].any? { |klass| property.kind_of?(klass) }
134
134
 
135
- case length = property.options.fetch(:length, DataMapper::Property::String::DEFAULT_LENGTH)
136
- when Range then options[:within] = length
137
- else options[:maximum] = length
135
+ length = property.options.fetch(:length, DataMapper::Property::String::DEFAULT_LENGTH)
136
+
137
+
138
+ if length.is_a?(Range)
139
+ raise ArgumentError, "Infinity is no valid upper bound for a length range" if length.last == Infinity
140
+ options[:within] = length
141
+ else
142
+ options[:maximum] = length
138
143
  end
139
144
 
140
145
  validates_length_of property.name, options_with_message(options, property, :length)
@@ -170,7 +175,7 @@ module DataMapper
170
175
  end
171
176
 
172
177
  def infer_type_validation_for(property, options)
173
- return if property.custom?
178
+ return if property.respond_to?(:custom?) && property.custom?
174
179
 
175
180
  if property.kind_of?(Property::Numeric)
176
181
  options[:gte] = property.min if property.min
@@ -43,7 +43,9 @@ module DataMapper
43
43
  contexts.clear
44
44
  end
45
45
 
46
- # Execute all validators in the named context against the target
46
+ # Execute all validators in the named context against the target. Load
47
+ # together any properties that are designated lazy but are not yet loaded.
48
+ # Optionally only validate dirty properties.
47
49
  #
48
50
  # @param [Symbol]
49
51
  # named_context the context we are validating against
@@ -54,9 +56,44 @@ module DataMapper
54
56
  def execute(named_context, target)
55
57
  target.errors.clear!
56
58
 
57
- context(named_context).map do |validator|
58
- validator.execute?(target) ? validator.call(target) : true
59
- end.all?
59
+ runnable_validators = context(named_context).select{ |validator| validator.execute?(target) }
60
+ validators = runnable_validators.dup
61
+
62
+ # By default we start the list with the full set of runnable validators.
63
+ #
64
+ # In the case of a new Resource or regular ruby class instance,
65
+ # everything needs to be validated completely, and no eager-loading
66
+ # logic should apply.
67
+ #
68
+ # In the case of a DM::Resource that isn't new, we optimize:
69
+ #
70
+ # 1. Eager-load all lazy, not-yet-loaded properties that need
71
+ # validation, all at once.
72
+ #
73
+ # 2. Limit run validators to
74
+ # - those applied to dirty attributes only,
75
+ # - those that should always run (presence/absence)
76
+ # - those that don't reference any real properties (field-less
77
+ # block validators)
78
+
79
+ if target.kind_of?(DataMapper::Resource) and !target.new?
80
+ dirty_attrs = target.dirty_attributes.keys.map{ |p| p.name }
81
+ validators = runnable_validators.select{ |v| dirty_attrs.include?(v.field_name) }
82
+
83
+ # Load all lazy, not-yet-loaded properties that need validation, all at once.
84
+ fields_to_load = validators.map{ |v| target.class.properties[v.field_name] }.select{ |p| p.lazy? && !p.loaded?(target) }
85
+ target.__send__(:eager_load, fields_to_load)
86
+
87
+ # Finally include any validators that should always run or don't
88
+ # reference any real properties (field-less block vaildators).
89
+ validators |= runnable_validators.select do |v|
90
+ [ MethodValidator, PresenceValidator, AbsenceValidator ].any? do |klass|
91
+ v.kind_of?(klass)
92
+ end
93
+ end
94
+ end
95
+
96
+ validators.map { |validator| validator.call(target) }.all?
60
97
  end
61
98
 
62
99
  end # module ContextualValidators
@@ -1,4 +1,4 @@
1
- # encoding: binary
1
+ # encoding: UTF-8
2
2
 
3
3
  module DataMapper
4
4
  module Validations
@@ -11,14 +11,19 @@ module DataMapper
11
11
  )
12
12
  end
13
13
 
14
- # RFC2822 (No attribution reference available)
14
+ # Almost RFC2822 (No attribution reference available).
15
+ #
16
+ # This differs in that it does not allow local domains (test@localhost).
17
+ # 99% of the time you do not want to allow these email addresses
18
+ # in a public web application.
15
19
  EmailAddress = begin
16
- alpha = "a-zA-Z"
20
+ alpha = "a-zA-Z\\p{Lu}\\p{Ll}" # Alpha characters, changed from RFC2822 to include unicode chars
17
21
  digit = "0-9"
18
22
  atext = "[#{alpha}#{digit}\!\#\$\%\&\'\*+\/\=\?\^\_\`\{\|\}\~\-]"
19
- dot_atom_text = "#{atext}+([.]#{atext}*)*"
23
+ dot_atom_text = "#{atext}+([.]#{atext}*)+" # Last char changed from * to +
20
24
  dot_atom = "#{dot_atom_text}"
21
- qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]'
25
+ no_ws_ctl = "\\x01-\\x08\\x11\\x12\\x14-\\x1f\\x7f"
26
+ qtext = "[^#{no_ws_ctl}\\x0d\\x22\\x5c]" # Non-whitespace, non-control character except for \ and "
22
27
  text = "[\\x01-\\x09\\x11\\x12\\x14-\\x7f]"
23
28
  quoted_pair = "(\\x5c#{text})"
24
29
  qcontent = "(?:#{qtext}|#{quoted_pair})"
@@ -27,14 +32,13 @@ module DataMapper
27
32
  word = "(?:#{atom}|#{quoted_string})"
28
33
  obs_local_part = "#{word}([.]#{word})*"
29
34
  local_part = "(?:#{dot_atom}|#{quoted_string}|#{obs_local_part})"
30
- no_ws_ctl = "\\x01-\\x08\\x11\\x12\\x14-\\x1f\\x7f"
31
35
  dtext = "[#{no_ws_ctl}\\x21-\\x5a\\x5e-\\x7e]"
32
36
  dcontent = "(?:#{dtext}|#{quoted_pair})"
33
37
  domain_literal = "\\[#{dcontent}+\\]"
34
- obs_domain = "#{atom}([.]#{atom})*"
38
+ obs_domain = "#{atom}([.]#{atom})+" # Last char changed from * to +
35
39
  domain = "(?:#{dot_atom}|#{domain_literal}|#{obs_domain})"
36
40
  addr_spec = "#{local_part}\@#{domain}"
37
- pattern = /^#{addr_spec}$/
41
+ pattern = /^#{addr_spec}$/u
38
42
  end
39
43
 
40
44
  end # module Email
@@ -109,6 +109,10 @@ module DataMapper
109
109
  errors.send(meth, *args, &block)
110
110
  end
111
111
 
112
+ def respond_to?(method)
113
+ super || errors.respond_to?(method)
114
+ end
115
+
112
116
  def [](property_name)
113
117
  if property_errors = errors[property_name.to_sym]
114
118
  property_errors
@@ -32,6 +32,10 @@ module DataMapper
32
32
  return true if allow_nil?(value)
33
33
  @options[:accept].include?(value)
34
34
  end
35
+
36
+ def allow_nil?(value)
37
+ @options[:allow_nil] && value.nil?
38
+ end
35
39
  end # class AcceptanceValidator
36
40
 
37
41
  module ValidatesAcceptance
@@ -1,8 +1,8 @@
1
1
  #require File.dirname(__FILE__) + '/formats/email'
2
2
 
3
3
  require 'pathname'
4
- require Pathname(__FILE__).dirname.expand_path + ".." + 'formats/email'
5
- require Pathname(__FILE__).dirname.expand_path + ".." + 'formats/url'
4
+ require 'dm-validations/formats/email'
5
+ require 'dm-validations/formats/url'
6
6
 
7
7
  module DataMapper
8
8
  module Validations
@@ -70,7 +70,7 @@ module DataMapper
70
70
  # @option :as<Format, Proc, Regexp> the pre-defined format, Proc or Regexp to validate against
71
71
  # @option :with<Format, Proc, Regexp> an alias for :as
72
72
  #
73
- # :email_address (format is specified in DataMapper::Validations::Format::Email)
73
+ # :email_address (format is specified in DataMapper::Validations::Format::Email - note that unicode emails will *not* be matched under MRI1.8.7)
74
74
  # :url (format is specified in DataMapper::Validations::Format::Url)
75
75
  #
76
76
  # @example [Usage]
@@ -90,30 +90,18 @@ module DataMapper
90
90
  end
91
91
  end
92
92
 
93
- # Test the value to see if it is blank or nil, and if it is allowed
93
+ # Test the value to see if it is blank or nil, and if it is allowed.
94
+ # Note that allowing blank without explicitly denying nil allows nil
95
+ # values, since nil.blank? is true.
94
96
  #
95
97
  # @param <Object> value to test
96
98
  # @return <Boolean> true if blank/nil is allowed, and the value is blank/nil
97
99
  def optional?(value)
98
- return allow_nil?(value) if value.nil?
99
- return allow_blank?(value) if value.blank?
100
- false
101
- end
102
-
103
- # Test if the value is nil and is allowed
104
- #
105
- # @param <Object> value to test
106
- # @return <Boolean> true if nil is allowed and value is nil
107
- def allow_nil?(value)
108
- @options[:allow_nil] if value.nil?
109
- end
110
-
111
- # Test if the value is blank and is allowed
112
- #
113
- # @param <Object> value to test
114
- # @return <Boolean> true if blank is allowed and value is blank
115
- def allow_blank?(value)
116
- @options[:allow_blank] if value.blank?
100
+ if value.nil?
101
+ @options[:allow_nil] || (@options[:allow_blank] && !@options.has_key?(:allow_nil))
102
+ elsif value.blank?
103
+ @options[:allow_blank]
104
+ end
117
105
  end
118
106
 
119
107
  # Returns true if validators are equal
@@ -94,7 +94,13 @@ module DataMapper
94
94
  #
95
95
  # @api private
96
96
  def value_length(value)
97
- value.to_str.split(//u).size
97
+ value.to_str.length
98
+ end
99
+
100
+ if RUBY_VERSION < '1.9'
101
+ def value_length(value)
102
+ value.to_str.scan(/./u).size
103
+ end
98
104
  end
99
105
 
100
106
  # Validate the value length is equal to the expected length
@@ -29,11 +29,17 @@ module DataMapper
29
29
  return true if optional?(value)
30
30
 
31
31
  opts = {
32
- :fields => target.model.key,
32
+ :fields => target.model.key(target.repository.name),
33
33
  field_name => value,
34
34
  }
35
35
 
36
- Array(@options[:scope]).each { |subject| opts[subject] = target.__send__(subject) }
36
+ Array(@options[:scope]).each {|subject|
37
+ if target.respond_to?(subject)
38
+ opts[subject] = target.__send__(subject)
39
+ else
40
+ raise ArgumentError, "Could not find property to scope by: #{subject}. Note that :unique does not currently support arbitrarily named groups, for that you should use :unique_index with an explicit validates_uniqueness_of."
41
+ end
42
+ }
37
43
 
38
44
  resource = DataMapper.repository(target.repository.name) { target.model.first(opts) }
39
45
 
@@ -1,6 +1,7 @@
1
1
  module DataMapper
2
2
  module Validations
3
3
  module Fixtures
4
+ # Mittelschauzer is a type of dog. The More You Know.
4
5
  class Mittelschnauzer
5
6
 
6
7
  #
@@ -6,57 +6,55 @@ describe "A model with a Boolean property" do
6
6
  @model = HasNullableBoolean.new(:id => 1)
7
7
  end
8
8
 
9
- describe "assigned to true" do
9
+ describe "assigned a true" do
10
10
  before :all do
11
- @model.set(:bool => true)
11
+ @model.bool = true
12
12
  end
13
13
 
14
14
  it_should_behave_like "valid model"
15
15
  end
16
16
 
17
- describe "assigned to false" do
17
+ describe "assigned a false" do
18
18
  before :all do
19
- @model.set(:bool => false)
19
+ @model.bool = false
20
20
  end
21
21
 
22
22
  it_should_behave_like "valid model"
23
23
  end
24
24
 
25
- describe "assigned to a nil" do
25
+ describe "assigned a nil" do
26
26
  before :all do
27
- @model.set(:bool => nil)
27
+ @model.bool = nil
28
28
  end
29
29
 
30
30
  it_should_behave_like "valid model"
31
31
  end
32
32
  end
33
33
 
34
-
35
-
36
34
  describe "A model with a required Boolean property" do
37
35
  before :all do
38
- @model = HasNotNullableBoolean.new(:id => 1)
36
+ @model = HasRequiredBoolean.new(:id => 1)
39
37
  end
40
38
 
41
- describe "assigned to true" do
39
+ describe "assigned a true" do
42
40
  before :all do
43
- @model.set(:bool => true)
41
+ @model.bool = true
44
42
  end
45
43
 
46
44
  it_should_behave_like "valid model"
47
45
  end
48
46
 
49
- describe "assigned to false" do
47
+ describe "assigned a false" do
50
48
  before :all do
51
- @model.set(:bool => false)
49
+ @model.bool = false
52
50
  end
53
51
 
54
52
  it_should_behave_like "valid model"
55
53
  end
56
54
 
57
- describe "assigned to a nil" do
55
+ describe "assigned a nil" do
58
56
  before :all do
59
- @model.set(:bool => nil)
57
+ @model.bool = nil
60
58
  end
61
59
 
62
60
  it_should_behave_like "invalid model"
@@ -67,32 +65,30 @@ describe "A model with a required Boolean property" do
67
65
  end
68
66
  end
69
67
 
70
-
71
-
72
68
  describe "A model with a required paranoid Boolean property" do
73
69
  before :all do
74
- @model = HasNotNullableParanoidBoolean.new(:id => 1)
70
+ @model = HasRequiredParanoidBoolean.new(:id => 1)
75
71
  end
76
72
 
77
- describe "assigned to true" do
73
+ describe "assigned a true" do
78
74
  before :all do
79
- @model.set(:bool => true)
75
+ @model.bool = true
80
76
  end
81
77
 
82
78
  it_should_behave_like "valid model"
83
79
  end
84
80
 
85
- describe "assigned to false" do
81
+ describe "assigned a false" do
86
82
  before :all do
87
- @model.set(:bool => false)
83
+ @model.bool = false
88
84
  end
89
85
 
90
86
  it_should_behave_like "valid model"
91
87
  end
92
88
 
93
- describe "assigned to a nil" do
89
+ describe "assigned a nil" do
94
90
  before :all do
95
- @model.set(:bool => nil)
91
+ @model.bool = nil
96
92
  end
97
93
 
98
94
  it_should_behave_like "invalid model"