durran-validatable 1.7.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +118 -0
- data/Rakefile +55 -0
- data/VERSION.yml +4 -0
- data/lib/child_validation.rb +15 -0
- data/lib/errors.rb +90 -0
- data/lib/included_validation.rb +9 -0
- data/lib/macros.rb +306 -0
- data/lib/object_extension.rb +21 -0
- data/lib/requireable.rb +26 -0
- data/lib/understandable.rb +31 -0
- data/lib/validatable.rb +25 -0
- data/lib/validatable_class_methods.rb +85 -0
- data/lib/validatable_instance_methods.rb +116 -0
- data/lib/validations/validates_acceptance_of.rb +14 -0
- data/lib/validations/validates_associated.rb +13 -0
- data/lib/validations/validates_confirmation_of.rb +23 -0
- data/lib/validations/validates_each.rb +14 -0
- data/lib/validations/validates_format_of.rb +16 -0
- data/lib/validations/validates_length_of.rb +30 -0
- data/lib/validations/validates_numericality_of.rb +27 -0
- data/lib/validations/validates_presence_of.rb +17 -0
- data/lib/validations/validates_true_for.rb +13 -0
- data/lib/validations/validation_base.rb +91 -0
- data/test/all_tests.rb +1 -0
- data/test/functional/test_validatable.rb +589 -0
- data/test/functional/test_validates_acceptance_of.rb +16 -0
- data/test/functional/test_validates_associated.rb +41 -0
- data/test/functional/test_validates_confirmation_of.rb +56 -0
- data/test/functional/test_validates_each.rb +14 -0
- data/test/functional/test_validates_format_of.rb +34 -0
- data/test/functional/test_validates_length_of.rb +64 -0
- data/test/functional/test_validates_numericality_of.rb +57 -0
- data/test/functional/test_validates_presence_of.rb +16 -0
- data/test/functional/test_validates_true_for.rb +27 -0
- data/test/test_helper.rb +33 -0
- data/test/unit/test_errors.rb +70 -0
- data/test/unit/test_understandable.rb +19 -0
- data/test/unit/test_validatable.rb +38 -0
- data/test/unit/test_validates_acceptance_of.rb +45 -0
- data/test/unit/test_validates_associated.rb +29 -0
- data/test/unit/test_validates_confirmation_of.rb +76 -0
- data/test/unit/test_validates_format_of.rb +44 -0
- data/test/unit/test_validates_length_of.rb +80 -0
- data/test/unit/test_validates_numericality_of.rb +76 -0
- data/test/unit/test_validates_presence_of.rb +35 -0
- data/test/unit/test_validates_true_for.rb +29 -0
- data/test/unit/test_validation_base.rb +52 -0
- metadata +126 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
class Object #:nodoc:
|
2
|
+
module InstanceExecHelper #:nodoc:
|
3
|
+
end
|
4
|
+
include InstanceExecHelper
|
5
|
+
def instance_eval_with_params(*args, &block)
|
6
|
+
begin
|
7
|
+
old_critical, Thread.critical = Thread.critical, true
|
8
|
+
n = 0
|
9
|
+
n += 1 while respond_to?(mname="__instance_exec#{n}")
|
10
|
+
InstanceExecHelper.module_eval{ define_method(mname, &block) }
|
11
|
+
ensure
|
12
|
+
Thread.critical = old_critical
|
13
|
+
end
|
14
|
+
begin
|
15
|
+
ret = send(mname, *args)
|
16
|
+
ensure
|
17
|
+
InstanceExecHelper.module_eval{ remove_method(mname) } rescue nil
|
18
|
+
end
|
19
|
+
ret
|
20
|
+
end
|
21
|
+
end
|
data/lib/requireable.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Validatable
|
2
|
+
module Requireable #:nodoc:
|
3
|
+
module ClassMethods #:nodoc:
|
4
|
+
def requires(*args)
|
5
|
+
required_options.concat args
|
6
|
+
end
|
7
|
+
|
8
|
+
def required_options
|
9
|
+
@required_options ||= []
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.included(klass)
|
14
|
+
klass.extend ClassMethods
|
15
|
+
end
|
16
|
+
|
17
|
+
def requires(options)
|
18
|
+
required_options = self.class.required_options.inject([]) do |errors, attribute|
|
19
|
+
errors << attribute.to_s unless options.has_key?(attribute)
|
20
|
+
errors
|
21
|
+
end
|
22
|
+
raise ArgumentError.new("#{self.class} requires options: #{required_options.join(', ')}") if required_options.any?
|
23
|
+
true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Validatable
|
2
|
+
module Understandable #:nodoc:
|
3
|
+
module ClassMethods #:nodoc:
|
4
|
+
def understands(*args)
|
5
|
+
understandings.concat args
|
6
|
+
end
|
7
|
+
|
8
|
+
def understandings
|
9
|
+
@understandings ||= []
|
10
|
+
end
|
11
|
+
|
12
|
+
def all_understandings
|
13
|
+
return understandings + self.superclass.all_understandings if self.superclass.respond_to? :all_understandings
|
14
|
+
understandings
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.included(klass)
|
19
|
+
klass.extend ClassMethods
|
20
|
+
end
|
21
|
+
|
22
|
+
def must_understand(hash)
|
23
|
+
invalid_options = hash.inject([]) do |errors, (key, value)|
|
24
|
+
errors << key.to_s unless self.class.all_understandings.include?(key)
|
25
|
+
errors
|
26
|
+
end
|
27
|
+
raise ArgumentError.new("invalid options: #{invalid_options.join(', ')}") if invalid_options.any?
|
28
|
+
true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/validatable.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'activesupport'
|
4
|
+
|
5
|
+
dir = File.expand_path(File.dirname(__FILE__))
|
6
|
+
|
7
|
+
require File.join(dir, 'object_extension')
|
8
|
+
require File.join(dir, 'errors')
|
9
|
+
require File.join(dir, 'validatable_class_methods')
|
10
|
+
require File.join(dir, 'macros')
|
11
|
+
require File.join(dir, 'validatable_instance_methods')
|
12
|
+
require File.join(dir, 'included_validation')
|
13
|
+
require File.join(dir, 'child_validation')
|
14
|
+
require File.join(dir, 'understandable')
|
15
|
+
require File.join(dir, 'requireable')
|
16
|
+
require File.join(dir, 'validations/validation_base')
|
17
|
+
require File.join(dir, 'validations/validates_format_of')
|
18
|
+
require File.join(dir, 'validations/validates_presence_of')
|
19
|
+
require File.join(dir, 'validations/validates_acceptance_of')
|
20
|
+
require File.join(dir, 'validations/validates_confirmation_of')
|
21
|
+
require File.join(dir, 'validations/validates_length_of')
|
22
|
+
require File.join(dir, 'validations/validates_true_for')
|
23
|
+
require File.join(dir, 'validations/validates_numericality_of')
|
24
|
+
require File.join(dir, 'validations/validates_each')
|
25
|
+
require File.join(dir, 'validations/validates_associated')
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Validatable
|
2
|
+
module ClassMethods #:nodoc:
|
3
|
+
|
4
|
+
def validate_children(instance, group)
|
5
|
+
self.children_to_validate.each do |child_validation|
|
6
|
+
next unless child_validation.should_validate?(instance)
|
7
|
+
child = instance.send child_validation.attribute
|
8
|
+
if (child.respond_to?(:valid_for_group?))
|
9
|
+
child.valid_for_group?(group)
|
10
|
+
else
|
11
|
+
child.valid?
|
12
|
+
end
|
13
|
+
child.errors.each do |attribute, messages|
|
14
|
+
if messages.is_a?(String)
|
15
|
+
add_error(instance, child_validation.map[attribute.to_sym] || attribute, messages)
|
16
|
+
else
|
17
|
+
messages.each do |message|
|
18
|
+
add_error(instance, child_validation.map[attribute.to_sym] || attribute, message)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def all_before_validations
|
26
|
+
if self.superclass.respond_to? :all_before_validations
|
27
|
+
return before_validations + self.superclass.all_before_validations
|
28
|
+
end
|
29
|
+
before_validations
|
30
|
+
end
|
31
|
+
|
32
|
+
def before_validations
|
33
|
+
@before_validations ||= []
|
34
|
+
end
|
35
|
+
|
36
|
+
def all_validations
|
37
|
+
if self.respond_to?(:superclass) && self.superclass.respond_to?(:all_validations)
|
38
|
+
return validations + self.superclass.all_validations
|
39
|
+
end
|
40
|
+
validations
|
41
|
+
end
|
42
|
+
|
43
|
+
def validations
|
44
|
+
@validations ||= []
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_error(instance, attribute, msg)
|
48
|
+
instance.errors.add(attribute, msg)
|
49
|
+
end
|
50
|
+
|
51
|
+
def validation_keys_include?(key)
|
52
|
+
validations.map { |validation| validation.key }.include?(key)
|
53
|
+
end
|
54
|
+
|
55
|
+
def validations_to_include
|
56
|
+
@validations_to_include ||= []
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
def add_validations(args, klass)
|
62
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
63
|
+
args.each do |attribute|
|
64
|
+
new_validation = klass.new self, attribute, options
|
65
|
+
self.validations << new_validation
|
66
|
+
self.create_valid_method_for_groups new_validation.groups
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_valid_method_for_groups(groups)
|
71
|
+
groups.each do |group|
|
72
|
+
self.class_eval do
|
73
|
+
define_method "valid_for_#{group}?".to_sym do
|
74
|
+
valid_for_group?(group)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def children_to_validate
|
81
|
+
@children_to_validate ||= []
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Validatable
|
2
|
+
def self.included(klass) #:nodoc:
|
3
|
+
klass.extend Validatable::ClassMethods
|
4
|
+
klass.extend Validatable::Macros
|
5
|
+
klass.class_eval do
|
6
|
+
include ActiveSupport::Callbacks
|
7
|
+
define_callbacks :validate, :validate_on_create, :validate_on_update
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# call-seq: valid?
|
12
|
+
#
|
13
|
+
# Returns true if no errors were added otherwise false. Only executes validations that have no :groups option specified
|
14
|
+
def valid?
|
15
|
+
valid_for_group?(nil)
|
16
|
+
end
|
17
|
+
|
18
|
+
# call-seq: errors
|
19
|
+
#
|
20
|
+
# Returns the Errors object that holds all information about attribute error messages.
|
21
|
+
def errors
|
22
|
+
@errors ||= Validatable::Errors.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def valid_for_group?(group) #:nodoc:
|
26
|
+
run_before_validations
|
27
|
+
errors.clear
|
28
|
+
run_callbacks(:validate)
|
29
|
+
|
30
|
+
if respond_to?(:new?)
|
31
|
+
new? ? run_callbacks(:validate_on_create) : run_callbacks(:validate_on_update)
|
32
|
+
end
|
33
|
+
|
34
|
+
self.class.validate_children(self, group)
|
35
|
+
self.validate_group(group)
|
36
|
+
errors.empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
def times_validated(key) #:nodoc:
|
40
|
+
times_validated_hash[key] || 0
|
41
|
+
end
|
42
|
+
|
43
|
+
def increment_times_validated_for(validation) #:nodoc:
|
44
|
+
if validation.key != nil
|
45
|
+
if times_validated_hash[validation.key].nil?
|
46
|
+
times_validated_hash[validation.key] = 1
|
47
|
+
else
|
48
|
+
times_validated_hash[validation.key] += 1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# call-seq: validate_only(key)
|
54
|
+
#
|
55
|
+
# Only executes a specified validation. The argument should follow a pattern based on the key of the validation.
|
56
|
+
# Examples:
|
57
|
+
# * validates_presence_of :name can be run with obj.validate_only("presence_of/name")
|
58
|
+
# * validates_presence_of :birthday, :key => "a key" can be run with obj.validate_only("presence_of/a key")
|
59
|
+
def validate_only(key)
|
60
|
+
validation_name, attribute_name = key.split("/")
|
61
|
+
validation_name = validation_name.split("_").collect{|word| word.capitalize}.join
|
62
|
+
validation_key = "#{self.class.name}/Validatable::Validates#{validation_name}/#{attribute_name}"
|
63
|
+
validation = self.class.all_validations.find { |validation| validation.key == validation_key }
|
64
|
+
raise ArgumentError.new("validation with key #{validation_key} could not be found") if validation.nil?
|
65
|
+
errors.clear
|
66
|
+
run_validation(validation)
|
67
|
+
end
|
68
|
+
|
69
|
+
protected
|
70
|
+
def times_validated_hash #:nodoc:
|
71
|
+
@times_validated_hash ||= {}
|
72
|
+
end
|
73
|
+
|
74
|
+
def validate_group(group) #:nodoc:
|
75
|
+
validation_levels.each do |level|
|
76
|
+
validations_for_level_and_group(level, group).each do |validation|
|
77
|
+
run_validation(validation) if validation.should_validate?(self)
|
78
|
+
end
|
79
|
+
return unless self.errors.empty?
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def run_validation(validation) #:nodoc:
|
84
|
+
validation_result = validation.valid?(self)
|
85
|
+
add_error(validation.attribute, validation.message(self)) unless validation_result
|
86
|
+
increment_times_validated_for(validation)
|
87
|
+
validation.run_after_validate(validation_result, self, validation.attribute)
|
88
|
+
end
|
89
|
+
|
90
|
+
def run_before_validations #:nodoc:
|
91
|
+
self.class.all_before_validations.each do |block|
|
92
|
+
instance_eval &block
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def add_error(attribute, message) #:nodoc:
|
97
|
+
self.class.add_error(self, attribute, message)
|
98
|
+
end
|
99
|
+
|
100
|
+
def validations_for_level_and_group(level, group) #:nodoc:
|
101
|
+
validations_for_level = self.all_validations.select { |validation| validation.level == level }
|
102
|
+
return validations_for_level.select { |validation| validation.groups.empty? } if group.nil?
|
103
|
+
validations_for_level.select { |validation| validation.groups.include?(group) }
|
104
|
+
end
|
105
|
+
|
106
|
+
def all_validations #:nodoc:
|
107
|
+
res = self.class.validations_to_include.inject(self.class.all_validations) do |result, included_validation_class|
|
108
|
+
result += self.send(included_validation_class.attribute).all_validations
|
109
|
+
result
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def validation_levels #:nodoc:
|
114
|
+
self.class.all_validations.inject([1]) { |result, validation| result << validation.level }.uniq.sort
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Validatable
|
2
|
+
class ValidatesAcceptanceOf < ValidationBase #:nodoc:
|
3
|
+
def valid?(instance)
|
4
|
+
value = instance.send(self.attribute)
|
5
|
+
return true if allow_nil && value.nil?
|
6
|
+
return true if allow_blank && value.blank?
|
7
|
+
%w(1 true t).include?(value)
|
8
|
+
end
|
9
|
+
|
10
|
+
def message(instance)
|
11
|
+
super || "must be accepted"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Validatable
|
2
|
+
class ValidatesAssociated < ValidationBase #:nodoc:
|
3
|
+
def valid?(instance)
|
4
|
+
Array(instance.send(attribute)).compact.map do |child|
|
5
|
+
child.valid?
|
6
|
+
end.all?
|
7
|
+
end
|
8
|
+
|
9
|
+
def message(instance)
|
10
|
+
super || "is invalid"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Validatable
|
2
|
+
class ValidatesConfirmationOf < ValidationBase #:nodoc:
|
3
|
+
option :case_sensitive
|
4
|
+
default :case_sensitive => true
|
5
|
+
|
6
|
+
def initialize(klass, attribute, options={})
|
7
|
+
klass.class_eval { attr_accessor "#{attribute}_confirmation" }
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def valid?(instance)
|
12
|
+
confirmation_value = instance.send("#{self.attribute}_confirmation")
|
13
|
+
return true if allow_nil && confirmation_value.nil?
|
14
|
+
return true if allow_blank && confirmation_value.blank?
|
15
|
+
return instance.send(self.attribute) == instance.send("#{self.attribute}_confirmation".to_sym) if case_sensitive
|
16
|
+
instance.send(self.attribute).to_s.casecmp(instance.send("#{self.attribute}_confirmation".to_sym).to_s) == 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def message(instance)
|
20
|
+
super || "doesn't match confirmation"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Validatable
|
2
|
+
class ValidatesEach < ValidationBase #:nodoc:
|
3
|
+
required_option :logic
|
4
|
+
|
5
|
+
def valid?(instance)
|
6
|
+
instance.instance_eval(&logic)
|
7
|
+
true # return true so no error is added. should look in the future at doing this different.
|
8
|
+
end
|
9
|
+
|
10
|
+
def message(instance)
|
11
|
+
super || "is invalid"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Validatable
|
2
|
+
class ValidatesFormatOf < ValidationBase #:nodoc:
|
3
|
+
required_option :with
|
4
|
+
|
5
|
+
def valid?(instance)
|
6
|
+
value = instance.send(self.attribute)
|
7
|
+
return true if allow_nil && value.nil?
|
8
|
+
return true if allow_blank && value.blank?
|
9
|
+
not (value.to_s =~ self.with).nil?
|
10
|
+
end
|
11
|
+
|
12
|
+
def message(instance)
|
13
|
+
super || "is invalid"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Validatable
|
2
|
+
class ValidatesLengthOf < ValidationBase #:nodoc:
|
3
|
+
option :minimum, :maximum, :is, :within
|
4
|
+
|
5
|
+
def message(instance)
|
6
|
+
super || "is invalid"
|
7
|
+
end
|
8
|
+
|
9
|
+
def valid?(instance)
|
10
|
+
valid = true
|
11
|
+
value = instance.send(self.attribute)
|
12
|
+
|
13
|
+
if value.nil?
|
14
|
+
return true if allow_nil
|
15
|
+
value = ''
|
16
|
+
end
|
17
|
+
|
18
|
+
if value.blank?
|
19
|
+
return true if allow_blank
|
20
|
+
value = ''
|
21
|
+
end
|
22
|
+
|
23
|
+
valid &&= value.length <= maximum unless maximum.nil?
|
24
|
+
valid &&= value.length >= minimum unless minimum.nil?
|
25
|
+
valid &&= value.length == is unless is.nil?
|
26
|
+
valid &&= within.include?(value.length) unless within.nil?
|
27
|
+
valid
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Validatable
|
2
|
+
class ValidatesNumericalityOf < ValidationBase #:nodoc:
|
3
|
+
option :only_integer
|
4
|
+
|
5
|
+
def valid?(instance)
|
6
|
+
value = value_for(instance)
|
7
|
+
return true if allow_nil && value.nil?
|
8
|
+
return true if allow_blank && value.blank?
|
9
|
+
|
10
|
+
value = value.to_s
|
11
|
+
regex = self.only_integer ? /\A[+-]?\d+\Z/ : /^\d*\.{0,1}\d+$/
|
12
|
+
not (value =~ regex).nil?
|
13
|
+
end
|
14
|
+
|
15
|
+
def message(instance)
|
16
|
+
super || "must be a number"
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
def value_for(instance)
|
21
|
+
before_typecast_method = "#{self.attribute}_before_typecast"
|
22
|
+
value_method = instance.respond_to?(before_typecast_method.intern) ? before_typecast_method : self.attribute
|
23
|
+
instance.send(value_method)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|