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,40 @@
1
+ module DataMapper
2
+ module Validate
3
+ module Format
4
+ module Email
5
+
6
+ def self.included(base)
7
+ DataMapper::Validate::FormatValidator::FORMATS.merge!(
8
+ :email_address => [ EmailAddress, lambda { |field, value| '%s is not a valid email address'.t(value) }]
9
+ )
10
+ end
11
+
12
+ EmailAddress = begin
13
+ alpha = "a-zA-Z"
14
+ digit = "0-9"
15
+ atext = "[#{alpha}#{digit}\!\#\$\%\&\'\*+\/\=\?\^\_\`\{\|\}\~\-]"
16
+ dot_atom_text = "#{atext}+([.]#{atext}*)*"
17
+ dot_atom = "#{dot_atom_text}"
18
+ qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]'
19
+ text = "[\\x01-\\x09\\x11\\x12\\x14-\\x7f]"
20
+ quoted_pair = "(\\x5c#{text})"
21
+ qcontent = "(?:#{qtext}|#{quoted_pair})"
22
+ quoted_string = "[\"]#{qcontent}+[\"]"
23
+ atom = "#{atext}+"
24
+ word = "(?:#{atom}|#{quoted_string})"
25
+ obs_local_part = "#{word}([.]#{word})*"
26
+ local_part = "(?:#{dot_atom}|#{quoted_string}|#{obs_local_part})"
27
+ no_ws_ctl = "\\x01-\\x08\\x11\\x12\\x14-\\x1f\\x7f"
28
+ dtext = "[#{no_ws_ctl}\\x21-\\x5a\\x5e-\\x7e]"
29
+ dcontent = "(?:#{dtext}|#{quoted_pair})"
30
+ domain_literal = "\\[#{dcontent}+\\]"
31
+ obs_domain = "#{atom}([.]#{atom})*"
32
+ domain = "(?:#{dot_atom}|#{domain_literal}|#{obs_domain})"
33
+ addr_spec = "#{local_part}\@#{domain}"
34
+ pattern = /^#{addr_spec}$/
35
+ end
36
+
37
+ end # module Email
38
+ end # module Format
39
+ end # module Validate
40
+ end # module DataMapper
@@ -0,0 +1,92 @@
1
+ module DataMapper
2
+ module Validate
3
+
4
+ # All validators extend this base class. Validators must:
5
+ #
6
+ # * Implement the initialize method to capture its parameters, also calling
7
+ # super to have this parent class capture the optional, general :if and
8
+ # :unless parameters.
9
+ # * Implement the call method, returning true or false. The call method
10
+ # provides the validation logic.
11
+ #
12
+ # @author Guy van den Berg
13
+ # @since 0.9
14
+ class GenericValidator
15
+
16
+ attr_accessor :if_clause
17
+ attr_accessor :unless_clause
18
+
19
+ # Construct a validator. Capture the :if and :unless clauses when present.
20
+ #
21
+ # @param field<String, Symbol> The property specified for validation
22
+ #
23
+ # @option :if<Symbol, Proc> The name of a method or a Proc to call to
24
+ # determine if the validation should occur.
25
+ # @option :unless<Symbol, Proc> The name of a method or a Proc to call to
26
+ # determine if the validation should not occur
27
+ # All additional key/value pairs are passed through to the validator
28
+ # that is sub-classing this GenericValidator
29
+ #
30
+ def initialize(field, opts = {})
31
+ @if_clause = opts.has_key?(:if) ? opts[:if] : nil
32
+ @unless_clause = opts.has_key?(:unless) ? opts[:unless] : nil
33
+ end
34
+
35
+ # Add an error message to a target resource. If the error corresponds to a
36
+ # specific field of the resource, add it to that field, otherwise add it
37
+ # as a :general message.
38
+ #
39
+ # @param <Object> target the resource that has the error
40
+ # @param <String> message the message to add
41
+ # @param <Symbol> field_name the name of the field that caused the error
42
+ #
43
+ # TODO - should the field_name for a general message be :default???
44
+ #
45
+ def add_error(target, message, field_name = :general)
46
+ target.errors.add(field_name,message)
47
+ end
48
+
49
+ # Call the validator. "call" is used so the operation is BoundMethod and
50
+ # Block compatible. This must be implemented in all concrete classes.
51
+ #
52
+ # @param <Object> target the resource that the validator must be called
53
+ # against
54
+ # @return <Boolean> true if valid, otherwise false
55
+ def call(target)
56
+ raise "DataMapper::Validate::GenericValidator::call must be overriden in #{self.class.to_s}"
57
+ end
58
+
59
+ def field_name
60
+ @field_name
61
+ end
62
+
63
+ # Determines if this validator should be run against the
64
+ # target by evaluating the :if and :unless clauses
65
+ # optionally passed while specifying any validator.
66
+ #
67
+ # @param <Object> target the resource that we check against
68
+ # @return <Boolean> true if should be run, otherwise false
69
+ def execute?(target)
70
+ return true if self.if_clause.nil? && self.unless_clause.nil?
71
+
72
+ if self.unless_clause
73
+ if self.unless_clause.is_a?(Symbol)
74
+ return false if target.send(self.unless_clause)
75
+ elsif self.unless_clause.respond_to?(:call)
76
+ return false if self.unless_clause.call(target)
77
+ end
78
+ end
79
+
80
+ if self.if_clause
81
+ if self.if_clause.is_a?(Symbol)
82
+ return target.send(self.if_clause)
83
+ elsif self.if_clause.respond_to?(:call)
84
+ return self.if_clause.call(target)
85
+ end
86
+ end
87
+ return true
88
+ end
89
+
90
+ end # class GenericValidator
91
+ end # module Validate
92
+ end # module DataMapper
@@ -0,0 +1,113 @@
1
+ module DataMapper
2
+ module Validate
3
+
4
+ ##
5
+ #
6
+ # @author Guy van den Berg
7
+ # @since 0.9
8
+ class LengthValidator < GenericValidator
9
+
10
+ def initialize(field_name, options)
11
+ super
12
+ @field_name = field_name
13
+ @options = options
14
+
15
+ @min = options[:minimum] || options[:min]
16
+ @max = options[:maximum] || options[:max]
17
+ @equal = options[:is] || options[:equals]
18
+ @range = options[:within] || options[:in]
19
+
20
+ @validation_method ||= :range if @range
21
+ @validation_method ||= :min if @min && @max.nil?
22
+ @validation_method ||= :max if @max && @min.nil?
23
+ @validation_method ||= :equals unless @equal.nil?
24
+ end
25
+
26
+ def call(target)
27
+ field_value = target.validation_property_value(@field_name)
28
+ return true if @options[:allow_nil] && field_value.nil?
29
+
30
+ field_value = '' if field_value.nil?
31
+
32
+ # XXX: HACK seems hacky to do this on every validation, probably should
33
+ # do this elsewhere?
34
+ field = Extlib::Inflection.humanize(@field_name)
35
+ min = @range ? @range.min : @min
36
+ max = @range ? @range.max : @max
37
+ equal = @equal
38
+
39
+ case @validation_method
40
+ when :range then
41
+ unless valid = @range.include?(field_value.size)
42
+ error_message = '%s must be between %s and %s characters long'.t(field, min, max)
43
+ end
44
+ when :min then
45
+ unless valid = field_value.size >= min
46
+ error_message = '%s must be more than %s characters long'.t(field, min)
47
+ end
48
+ when :max then
49
+ unless valid = field_value.size <= max
50
+ error_message = '%s must be less than %s characters long'.t(field, max)
51
+ end
52
+ when :equals then
53
+ unless valid = field_value.size == equal
54
+ error_message = '%s must be %s characters long'.t(field, equal)
55
+ end
56
+ end
57
+
58
+ error_message ||= @options[:message]
59
+
60
+ add_error(target, error_message, @field_name) unless valid
61
+
62
+ return valid
63
+ end
64
+
65
+ end # class LengthValidator
66
+
67
+ module ValidatesLength
68
+
69
+ # Validates that the length of the attribute is equal to, less than,
70
+ # greater than or within a certain range (depending upon the options
71
+ # you specify).
72
+ #
73
+ # @option :allow_nil<Boolean> true/false (default is true)
74
+ # @option :minimum ensures that the attribute's length is greater than
75
+ # or equal to the supplied value
76
+ # @option :min alias for :minimum
77
+ # @option :maximum ensures the attribute's length is less than or equal
78
+ # to the supplied value
79
+ # @option :max alias for :maximum
80
+ # @option :equals ensures the attribute's length is equal to the
81
+ # supplied value
82
+ # @option :is alias for :equals
83
+ # @option :in<Range> given a Range, ensures that the attributes length is
84
+ # include?'ed in the Range
85
+ # @option :within<Range> alias for :in
86
+ #
87
+ # @example [Usage]
88
+ # require 'dm-validations'
89
+ #
90
+ # class Page
91
+ # include DataMapper::Resource
92
+ #
93
+ # property high, Integer
94
+ # property low, Integer
95
+ # property just_right, Integer
96
+ #
97
+ # validates_length :high, :min => 100000000000
98
+ # validates_length :low, :equals => 0
99
+ # validates_length :just_right, :within => 1..10
100
+ #
101
+ # # a call to valid? will return false unless:
102
+ # # high is greater than or equal to 100000000000
103
+ # # low is equal to 0
104
+ # # just_right is between 1 and 10 (inclusive of both 1 and 10)
105
+ #
106
+ def validates_length(*fields)
107
+ opts = opts_from_validator_args(fields)
108
+ add_validator_to_context(opts, fields, DataMapper::Validate::LengthValidator)
109
+ end
110
+
111
+ end # module ValidatesLength
112
+ end # module Validate
113
+ end # module DataMapper
@@ -0,0 +1,58 @@
1
+ module DataMapper
2
+ module Validate
3
+
4
+ ##
5
+ #
6
+ # @author Guy van den Berg
7
+ # @since 0.9
8
+ class MethodValidator < GenericValidator
9
+
10
+ def initialize(method_name, options={})
11
+ super
12
+ @method_name, @options = method_name, options
13
+ @options[:integer_only] = false unless @options.has_key?(:integer_only)
14
+ end
15
+
16
+ def call(target)
17
+ result,message = target.send(@method_name)
18
+ add_error(target,message,@method_name) if !result
19
+ result
20
+ end
21
+ end # class MethodValidator
22
+
23
+ module ValidatesWithMethod
24
+
25
+ ##
26
+ # Validate using the given method. The method given needs to return:
27
+ # [result::<Boolean>, Error Message::<String>]
28
+ #
29
+ # @example [Usage]
30
+ # require 'dm-validations'
31
+ #
32
+ # class Page
33
+ # include DataMapper::Resource
34
+ #
35
+ # property :zip_code, String
36
+ #
37
+ # validates_with_method :in_the_right_location?
38
+ #
39
+ # def in_the_right_location?
40
+ # if @zip_code == "94301"
41
+ # return true
42
+ # else
43
+ # return [false, "You're in the wrong zip code"]
44
+ # end
45
+ # end
46
+ #
47
+ # # A call to valid? will return false and
48
+ # # populate the object's errors with "You're in the
49
+ # # wrong zip code" unless zip_code == "94301"
50
+ #
51
+ def validates_with_method(*fields)
52
+ opts = opts_from_validator_args(fields)
53
+ add_validator_to_context(opts, fields, DataMapper::Validate::MethodValidator)
54
+ end
55
+
56
+ end # module ValidatesWithMethod
57
+ end # module Validate
58
+ end # module DataMapper
@@ -0,0 +1,66 @@
1
+ module DataMapper
2
+ module Validate
3
+
4
+ ##
5
+ #
6
+ # @author Guy van den Berg
7
+ # @since 0.9
8
+ class NumericValidator < GenericValidator
9
+
10
+ def initialize(field_name, options={})
11
+ super
12
+ @field_name, @options = field_name, options
13
+ @options[:integer_only] = false unless @options.has_key?(:integer_only)
14
+ end
15
+
16
+ def call(target)
17
+ value = target.send(field_name)
18
+ return true if @options[:allow_nil] && value.nil?
19
+
20
+ value = value.kind_of?(BigDecimal) ? value.to_s('F') : value.to_s
21
+
22
+ error_message = @options[:message]
23
+ precision = @options[:precision]
24
+ scale = @options[:scale]
25
+
26
+ if @options[:integer_only]
27
+ return true if value =~ /\A[+-]?\d+\z/
28
+ error_message ||= '%s must be an integer'.t(Extlib::Inflection.humanize(@field_name))
29
+ else
30
+ # FIXME: if precision and scale are not specified, can we assume that it is an integer?
31
+ if precision && scale
32
+ if precision > scale && scale > 0
33
+ return true if value =~ /\A[+-]?(?:\d{1,#{precision - scale}}|\d{0,#{precision - scale}}\.\d{1,#{scale}})\z/
34
+ elsif precision > scale && scale == 0
35
+ return true if value =~ /\A[+-]?(?:\d{1,#{precision}}(?:\.0)?)\z/
36
+ elsif precision == scale
37
+ return true if value =~ /\A[+-]?(?:0(?:\.\d{1,#{scale}})?)\z/
38
+ else
39
+ raise ArgumentError, "Invalid precision #{precision.inspect} and scale #{scale.inspect} for #{field_name} (value: #{value.inspect} #{value.class})"
40
+ end
41
+ else
42
+ return true if value =~ /\A[+-]?(?:\d+|\d*\.\d+)\z/
43
+ end
44
+ error_message ||= '%s must be a number'.t(Extlib::Inflection.humanize(@field_name))
45
+ end
46
+
47
+ add_error(target, error_message, @field_name)
48
+
49
+ # TODO: check the gt, gte, lt, lte, and eq options
50
+
51
+ return false
52
+ end
53
+ end # class NumericValidator
54
+
55
+ module ValidatesIsNumber
56
+
57
+ # Validate whether a field is numeric
58
+ #
59
+ def validates_is_number(*fields)
60
+ opts = opts_from_validator_args(fields)
61
+ add_validator_to_context(opts, fields, DataMapper::Validate::NumericValidator)
62
+ end
63
+
64
+ end # module ValidatesIsNumber
65
+ end # module Validate
66
+ end # module DataMapper
@@ -0,0 +1,60 @@
1
+ module DataMapper
2
+ module Validate
3
+
4
+ ##
5
+ #
6
+ # @author Dirkjan Bussink
7
+ # @since 0.9
8
+ class PrimitiveValidator < GenericValidator
9
+
10
+ def initialize(field_name, options={})
11
+ super
12
+ @field_name, @options = field_name, options
13
+ end
14
+
15
+ def call(target)
16
+ value = target.validation_property_value(@field_name)
17
+ property = target.validation_property(@field_name)
18
+ return true if value.nil? || value.kind_of?(property.primitive)
19
+
20
+ error_message = @options[:message] || default_error(property)
21
+ add_error(target, error_message, @field_name)
22
+
23
+ false
24
+ end
25
+
26
+ protected
27
+
28
+ def default_error(property)
29
+ "%s must be of type #{property.primitive.to_s}".t(Extlib::Inflection.humanize(@field_name))
30
+ end
31
+
32
+ end # class PrimitiveValidator
33
+
34
+ module ValidatesIsPrimitive
35
+
36
+ ##
37
+ # Validates that the specified attribute is of the correct primitive type.
38
+ #
39
+ # @example [Usage]
40
+ # require 'dm-validations'
41
+ #
42
+ # class Person
43
+ # include DataMapper::Resource
44
+ #
45
+ # property :birth_date, Date
46
+ #
47
+ # validates_is_primitive :birth_date
48
+ #
49
+ # # a call to valid? will return false unless
50
+ # # the birth_date is something that can be properly
51
+ # # casted into a Date object.
52
+ # end
53
+ def validates_is_primitive(*fields)
54
+ opts = opts_from_validator_args(fields)
55
+ add_validator_to_context(opts, fields, DataMapper::Validate::PrimitiveValidator)
56
+ end
57
+
58
+ end # module ValidatesPresent
59
+ end # module Validate
60
+ end # module DataMapper
@@ -0,0 +1,88 @@
1
+ module DataMapper
2
+ module Validate
3
+
4
+ ##
5
+ #
6
+ # @author Guy van den Berg
7
+ # @since 0.9
8
+ class RequiredFieldValidator < GenericValidator
9
+
10
+ def initialize(field_name, options={})
11
+ super
12
+ @field_name, @options = field_name, options
13
+ end
14
+
15
+ def call(target)
16
+ value = target.validation_property_value(@field_name)
17
+ property = target.validation_property(@field_name)
18
+ return true if present?(value, property)
19
+
20
+ error_message = @options[:message] || default_error(property)
21
+ add_error(target, error_message, @field_name)
22
+
23
+ false
24
+ end
25
+
26
+ protected
27
+
28
+ # Boolean property types are considered present if non-nil.
29
+ # Other property types are considered present if non-blank.
30
+ # Non-properties are considered present if non-blank.
31
+ def present?(value, property)
32
+ boolean_type?(property) ? !value.nil? : !value.blank?
33
+ end
34
+
35
+ def default_error(property)
36
+ actual = boolean_type?(property) ? "nil" : "blank"
37
+ "%s must not be #{actual}".t(Extlib::Inflection.humanize(@field_name))
38
+ end
39
+
40
+ # Is +property+ a boolean property?
41
+ #
42
+ # Returns true for Boolean, ParanoidBoolean, TrueClass, etc. properties.
43
+ # Returns false for other property types.
44
+ # Returns false for non-properties.
45
+ def boolean_type?(property)
46
+ property ? property.primitive == TrueClass : false
47
+ end
48
+
49
+ end # class RequiredFieldValidator
50
+
51
+ module ValidatesPresent
52
+
53
+ ##
54
+ # Validates that the specified attribute is present.
55
+ #
56
+ # For most property types "being present" is the same as being "not
57
+ # blank" as determined by the attribute's #blank? method. However, in
58
+ # the case of Boolean, "being present" means not nil; i.e. true or
59
+ # false.
60
+ #
61
+ # @note
62
+ # dm-core's support lib adds the blank? method to many classes,
63
+ # @see lib/dm-core/support/blank.rb (dm-core) for more information.
64
+ #
65
+ # @example [Usage]
66
+ # require 'dm-validations'
67
+ #
68
+ # class Page
69
+ # include DataMapper::Resource
70
+ #
71
+ # property :required_attribute, String
72
+ # property :another_required, String
73
+ # property :yet_again, String
74
+ #
75
+ # validates_present :required_attribute
76
+ # validates_present :another_required, :yet_again
77
+ #
78
+ # # a call to valid? will return false unless
79
+ # # all three attributes are !blank?
80
+ # end
81
+ def validates_present(*fields)
82
+ opts = opts_from_validator_args(fields)
83
+ add_validator_to_context(opts, fields, DataMapper::Validate::RequiredFieldValidator)
84
+ end
85
+
86
+ end # module ValidatesPresent
87
+ end # module Validate
88
+ end # module DataMapper