hashrocket-validatable 1.7.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.
- data/README.rdoc +118 -0
- data/Rakefile +54 -0
- data/VERSION.yml +4 -0
- data/lib/child_validation.rb +15 -0
- data/lib/errors.rb +88 -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_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_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 +64 -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_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 +118 -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,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
|
+
|