dm-validations 1.0.2 → 1.1.0.rc1

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 (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"