hanami-validations 0.0.0 → 0.5.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +89 -0
- data/LICENSE.md +22 -0
- data/README.md +559 -7
- data/hanami-validations.gemspec +16 -12
- data/lib/hanami-validations.rb +1 -0
- data/lib/hanami/validations.rb +300 -2
- data/lib/hanami/validations/attribute.rb +252 -0
- data/lib/hanami/validations/attribute_definer.rb +526 -0
- data/lib/hanami/validations/blank_value_checker.rb +55 -0
- data/lib/hanami/validations/coercions.rb +31 -0
- data/lib/hanami/validations/error.rb +95 -0
- data/lib/hanami/validations/errors.rb +155 -0
- data/lib/hanami/validations/nested_attributes.rb +22 -0
- data/lib/hanami/validations/validation_set.rb +81 -0
- data/lib/hanami/validations/validator.rb +29 -0
- data/lib/hanami/validations/version.rb +2 -1
- metadata +57 -16
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/bin/console +0 -14
- data/bin/setup +0 -8
@@ -0,0 +1,55 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Validations
|
3
|
+
# @since 0.2.2
|
4
|
+
# @api private
|
5
|
+
class BlankValueChecker
|
6
|
+
# @since 0.2.2
|
7
|
+
# @api private
|
8
|
+
BLANK_STRING_MATCHER = /\A[[:space:]]*\z/.freeze
|
9
|
+
|
10
|
+
def initialize(value)
|
11
|
+
@value = value
|
12
|
+
end
|
13
|
+
|
14
|
+
# Checks if the value is "blank".
|
15
|
+
#
|
16
|
+
# @since 0.2.2
|
17
|
+
# @api private
|
18
|
+
def blank_value?
|
19
|
+
_nil_value? || _blank_string? || _empty_value?
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# Checks if the value is `nil`.
|
25
|
+
#
|
26
|
+
# @since 0.2.2
|
27
|
+
# @api private
|
28
|
+
def _nil_value?
|
29
|
+
@value.nil?
|
30
|
+
end
|
31
|
+
|
32
|
+
# @since 0.2.2
|
33
|
+
# @api private
|
34
|
+
def _blank_string?
|
35
|
+
(@value.respond_to?(:match) and @value.match(BLANK_STRING_MATCHER))
|
36
|
+
end
|
37
|
+
|
38
|
+
# @since 0.2.2
|
39
|
+
# @api private
|
40
|
+
def _empty_value?
|
41
|
+
return false if _enumerable?
|
42
|
+
(@value.respond_to?(:empty?) and @value.empty?)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Collectable classes should not be considered as blank value
|
46
|
+
# even if it's responds _true_ to its own `empty?` method.
|
47
|
+
#
|
48
|
+
# @since 0.4.0
|
49
|
+
# @api private
|
50
|
+
def _enumerable?
|
51
|
+
@value.respond_to?(:each)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'hanami/utils/kernel'
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
module Validations
|
5
|
+
# Coercions for attribute's values.
|
6
|
+
#
|
7
|
+
# @since 0.1.0
|
8
|
+
# @api private
|
9
|
+
module Coercions
|
10
|
+
# Coerces the given values with the given type
|
11
|
+
#
|
12
|
+
# @param coercer [Class] the type
|
13
|
+
# @param value [Array] of objects to be coerced
|
14
|
+
# @param blk [Proc] an optional block to pass to the custom coercer
|
15
|
+
#
|
16
|
+
# @return [Object,nil] The result of the coercion, if possible
|
17
|
+
#
|
18
|
+
# @raise [ArgumentError] if the custom coercer's `#initialize` has a wrong arity.
|
19
|
+
#
|
20
|
+
# @since 0.1.0
|
21
|
+
# @api private
|
22
|
+
def self.coerce(coercer, value, &blk)
|
23
|
+
if ::Hanami::Utils::Kernel.respond_to?(coercer.to_s)
|
24
|
+
::Hanami::Utils::Kernel.__send__(coercer.to_s, value, &blk) rescue nil
|
25
|
+
else
|
26
|
+
coercer.new(value, &blk)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Validations
|
3
|
+
# A single validation error for an attribute
|
4
|
+
#
|
5
|
+
# @since 0.1.0
|
6
|
+
class Error
|
7
|
+
# @since 0.2.4
|
8
|
+
# @api private
|
9
|
+
NAMESPACE_SEPARATOR = '.'.freeze
|
10
|
+
|
11
|
+
# The name of the attribute
|
12
|
+
#
|
13
|
+
# @return [Symbol] the name of the attribute
|
14
|
+
#
|
15
|
+
# @since 0.2.4
|
16
|
+
#
|
17
|
+
# @see Hanami::Validations::Error#attribute
|
18
|
+
#
|
19
|
+
# @example
|
20
|
+
# error = Error.new(:name, :presence, true, nil, 'author')
|
21
|
+
#
|
22
|
+
# error.attribute # => "author.name"
|
23
|
+
# error.attribute_name # => "name"
|
24
|
+
attr_reader :attribute_name
|
25
|
+
|
26
|
+
# The name of the validation
|
27
|
+
#
|
28
|
+
# @return [Symbol] the name of the validation
|
29
|
+
#
|
30
|
+
# @since 0.1.0
|
31
|
+
attr_reader :validation
|
32
|
+
|
33
|
+
# The expected value
|
34
|
+
#
|
35
|
+
# @return [Object] the expected value
|
36
|
+
#
|
37
|
+
# @since 0.1.0
|
38
|
+
attr_reader :expected
|
39
|
+
|
40
|
+
# The actual value
|
41
|
+
#
|
42
|
+
# @return [Object] the actual value
|
43
|
+
#
|
44
|
+
# @since 0.1.0
|
45
|
+
attr_reader :actual
|
46
|
+
|
47
|
+
# Returns the namespaced attribute name
|
48
|
+
#
|
49
|
+
# In cases where the error was pulled up from nested validators,
|
50
|
+
# `attribute` will be a namespaced string containing
|
51
|
+
# parent attribute names separated by a period.
|
52
|
+
#
|
53
|
+
# @since 0.1.0
|
54
|
+
#
|
55
|
+
# @see Hanami::Validations::Error#attribute_name
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# error = Error.new(:name, :presence, true, nil, 'author')
|
59
|
+
#
|
60
|
+
# error.attribute # => "author.name"
|
61
|
+
# error.attribute_name # => "name"
|
62
|
+
attr_accessor :attribute
|
63
|
+
|
64
|
+
# Initialize a validation error
|
65
|
+
#
|
66
|
+
# @param attribute_name [Symbol] the name of the attribute
|
67
|
+
# @param validation [Symbol] the name of the validation
|
68
|
+
# @param expected [Object] the expected value
|
69
|
+
# @param actual [Object] the actual value
|
70
|
+
# @param namespace [String] the optional namespace
|
71
|
+
#
|
72
|
+
# @since 0.1.0
|
73
|
+
# @api private
|
74
|
+
def initialize(attribute_name, validation, expected, actual, namespace = nil)
|
75
|
+
@attribute_name = attribute_name.to_s
|
76
|
+
@validation = validation
|
77
|
+
@expected = expected
|
78
|
+
@actual = actual
|
79
|
+
@namespace = namespace
|
80
|
+
@attribute = [@namespace, attribute_name].compact.join(NAMESPACE_SEPARATOR)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Check if self equals to `other`
|
84
|
+
#
|
85
|
+
# @since 0.1.0
|
86
|
+
def ==(other)
|
87
|
+
other.is_a?(self.class) &&
|
88
|
+
other.attribute == attribute &&
|
89
|
+
other.validation == validation &&
|
90
|
+
other.expected == expected &&
|
91
|
+
other.actual == actual
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'hanami/validations/error'
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
module Validations
|
5
|
+
# A set of errors for a validator
|
6
|
+
#
|
7
|
+
# This is the result of calling `#valid?` on a validator.
|
8
|
+
#
|
9
|
+
# @see Hanami::Validations::Error
|
10
|
+
#
|
11
|
+
# @since 0.1.0
|
12
|
+
class Errors
|
13
|
+
# Initialize the errors
|
14
|
+
#
|
15
|
+
# @since 0.1.0
|
16
|
+
# @api private
|
17
|
+
def initialize
|
18
|
+
@errors = Hash.new
|
19
|
+
end
|
20
|
+
|
21
|
+
# Check if the set is empty
|
22
|
+
#
|
23
|
+
# @return [TrueClass,FalseClass] the result of the check
|
24
|
+
#
|
25
|
+
# @since 0.1.0
|
26
|
+
#
|
27
|
+
# @see Hanami::Validations::Errors#any?
|
28
|
+
def empty?
|
29
|
+
@errors.empty?
|
30
|
+
end
|
31
|
+
|
32
|
+
# Check if the set has any entry
|
33
|
+
#
|
34
|
+
# @return [TrueClass,FalseClass] the result of the check
|
35
|
+
#
|
36
|
+
# @since 0.2.0
|
37
|
+
#
|
38
|
+
# @see Hanami::Validations::Errors#empty?
|
39
|
+
def any?
|
40
|
+
@errors.any?
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns how many validations have failed
|
44
|
+
#
|
45
|
+
# @return [Fixnum] the count
|
46
|
+
#
|
47
|
+
# @since 0.1.0
|
48
|
+
def count
|
49
|
+
errors.count
|
50
|
+
end
|
51
|
+
|
52
|
+
alias_method :size, :count
|
53
|
+
|
54
|
+
# Clears the internal state of the errors
|
55
|
+
#
|
56
|
+
# @since 0.1.0
|
57
|
+
# @api private
|
58
|
+
def clear
|
59
|
+
@errors.clear
|
60
|
+
end
|
61
|
+
|
62
|
+
# Iterate thru the errors and yields the given block
|
63
|
+
#
|
64
|
+
# @param blk [Proc] the given block
|
65
|
+
# @yield [error] a Hanami::Validations::Error
|
66
|
+
#
|
67
|
+
# @see Hanami::Validations::Error
|
68
|
+
#
|
69
|
+
# @since 0.1.0
|
70
|
+
def each(&blk)
|
71
|
+
errors.each(&blk)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Iterate thru the errors, yields the given block and collect the
|
75
|
+
# returning value.
|
76
|
+
#
|
77
|
+
# @param blk [Proc] the given block
|
78
|
+
# @yield [error] a Hanami::Validations::Error
|
79
|
+
#
|
80
|
+
# @see Hanami::Validations::Error
|
81
|
+
#
|
82
|
+
# @since 0.1.0
|
83
|
+
def map(&blk)
|
84
|
+
errors.map(&blk)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Add an error to the set
|
88
|
+
#
|
89
|
+
# @param attribute [Symbol] the name of the attribute
|
90
|
+
# @param errors [Array] a collection of errors
|
91
|
+
#
|
92
|
+
# @since 0.1.0
|
93
|
+
# @api private
|
94
|
+
#
|
95
|
+
# @see Hanami::Validations::Error
|
96
|
+
def add(attribute, *errors)
|
97
|
+
if errors.any?
|
98
|
+
@errors[attribute] ||= []
|
99
|
+
@errors[attribute].push(*errors)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Return the errors for the given attribute
|
104
|
+
#
|
105
|
+
# @param attribute [Symbol] the name of the attribute
|
106
|
+
#
|
107
|
+
# @since 0.1.0
|
108
|
+
def for(attribute)
|
109
|
+
@errors.fetch(attribute) { [] }
|
110
|
+
end
|
111
|
+
|
112
|
+
# Check if the current set of errors equals to the one who belongs to
|
113
|
+
# `other`.
|
114
|
+
#
|
115
|
+
# @param other [Object] the other term of comparison
|
116
|
+
#
|
117
|
+
# @return [TrueClass,FalseClass] the result of comparison
|
118
|
+
#
|
119
|
+
# @since 0.1.0
|
120
|
+
def ==(other)
|
121
|
+
other.is_a?(self.class) &&
|
122
|
+
other.errors == errors
|
123
|
+
end
|
124
|
+
|
125
|
+
alias_method :eql?, :==
|
126
|
+
|
127
|
+
# Return a serializable Hash representation of the errors.
|
128
|
+
#
|
129
|
+
# @return [Hanami::Utils::Hash] the Hash
|
130
|
+
#
|
131
|
+
# @since 0.2.1
|
132
|
+
def to_h
|
133
|
+
Utils::Hash.new(@errors).deep_dup
|
134
|
+
end
|
135
|
+
|
136
|
+
# Return a flat collection of errors.
|
137
|
+
#
|
138
|
+
# @return [Array]
|
139
|
+
#
|
140
|
+
# @since 0.2.1
|
141
|
+
def to_a
|
142
|
+
errors.dup
|
143
|
+
end
|
144
|
+
|
145
|
+
protected
|
146
|
+
# A flatten set of errors for all the attributes
|
147
|
+
#
|
148
|
+
# @since 0.1.0
|
149
|
+
# @api private
|
150
|
+
def errors
|
151
|
+
@errors.values.flatten
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Validations
|
3
|
+
# @since 0.3.1
|
4
|
+
# @api private
|
5
|
+
class NestedAttributes
|
6
|
+
# @since 0.3.1
|
7
|
+
# @api private
|
8
|
+
def self.fabricate(&blk)
|
9
|
+
dup.tap do |klass|
|
10
|
+
klass.class_eval { include Hanami::Validations }
|
11
|
+
klass.class_eval(&blk)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# @since 0.3.1
|
16
|
+
# @api private
|
17
|
+
def hanami_nested_attributes?
|
18
|
+
true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Validations
|
3
|
+
# A set of validations defined on an object
|
4
|
+
#
|
5
|
+
# @since 0.2.2
|
6
|
+
# @api private
|
7
|
+
class ValidationSet
|
8
|
+
# Allowed validations
|
9
|
+
#
|
10
|
+
# @since 0.2.2
|
11
|
+
# @api private
|
12
|
+
VALIDATIONS = [
|
13
|
+
:presence,
|
14
|
+
:acceptance,
|
15
|
+
:format,
|
16
|
+
:inclusion,
|
17
|
+
:exclusion,
|
18
|
+
:confirmation,
|
19
|
+
:size,
|
20
|
+
:type,
|
21
|
+
:nested
|
22
|
+
].freeze
|
23
|
+
|
24
|
+
# @since 0.2.2
|
25
|
+
# @api private
|
26
|
+
def initialize
|
27
|
+
@validations = Hash.new {|h,k| h[k] = {} }
|
28
|
+
end
|
29
|
+
|
30
|
+
# @since 0.2.2
|
31
|
+
# @api private
|
32
|
+
def add(name, options)
|
33
|
+
@validations[name.to_sym].merge!(
|
34
|
+
validate_options!(name, options)
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
# @since 0.2.2
|
39
|
+
# @api private
|
40
|
+
def each(&blk)
|
41
|
+
@validations.each(&blk)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @since 0.2.2
|
45
|
+
# @api private
|
46
|
+
def each_key(&blk)
|
47
|
+
@validations.each_key(&blk)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @since 0.2.3
|
51
|
+
# @api private
|
52
|
+
def names
|
53
|
+
@validations.keys
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
# Checks at the loading time if the user defined validations are recognized
|
58
|
+
#
|
59
|
+
# @param name [Symbol] the attribute name
|
60
|
+
# @param options [Hash] the set of validations associated with the given attribute
|
61
|
+
#
|
62
|
+
# @raise [ArgumentError] if at least one of the validations are not
|
63
|
+
# recognized
|
64
|
+
#
|
65
|
+
# @since 0.2.2
|
66
|
+
# @api private
|
67
|
+
def validate_options!(name, options)
|
68
|
+
if (unknown = (options.keys - VALIDATIONS)) && unknown.any?
|
69
|
+
raise ArgumentError.new(%(Unknown validation(s): #{ unknown.join ', ' } for "#{ name }" attribute))
|
70
|
+
end
|
71
|
+
|
72
|
+
# FIXME remove
|
73
|
+
if options[:confirmation]
|
74
|
+
add(:"#{ name }_confirmation", {})
|
75
|
+
end
|
76
|
+
|
77
|
+
options
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Validations
|
3
|
+
# Validate given validations and return a set of errors
|
4
|
+
#
|
5
|
+
# @since 0.2.2
|
6
|
+
# @api private
|
7
|
+
class Validator
|
8
|
+
def initialize(validation_set, attributes, errors)
|
9
|
+
@validation_set = validation_set
|
10
|
+
@attributes = attributes
|
11
|
+
@errors = errors
|
12
|
+
end
|
13
|
+
|
14
|
+
# @since 0.2.2
|
15
|
+
# @api private
|
16
|
+
def validate
|
17
|
+
@errors.clear
|
18
|
+
@validation_set.each do |name, validations|
|
19
|
+
value = @attributes[name]
|
20
|
+
value = @attributes[name.to_s] if value.nil?
|
21
|
+
|
22
|
+
attribute = Attribute.new(@attributes, name, value, validations, @errors)
|
23
|
+
attribute.validate
|
24
|
+
end
|
25
|
+
@errors
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|