dm-validations 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
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