lotus-validations 0.0.0 → 0.1.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/{LICENSE.txt → LICENSE.md} +0 -0
- data/README.md +366 -3
- data/lib/lotus-validations.rb +1 -0
- data/lib/lotus/validations.rb +393 -2
- data/lib/lotus/validations/attribute_validator.rb +280 -0
- data/lib/lotus/validations/coercions.rb +30 -0
- data/lib/lotus/validations/error.rb +61 -0
- data/lib/lotus/validations/errors.rb +123 -0
- data/lib/lotus/validations/version.rb +2 -1
- data/lotus-validations.gemspec +15 -11
- metadata +41 -11
- data/.gitignore +0 -14
- data/Gemfile +0 -4
- data/Rakefile +0 -2
@@ -0,0 +1,280 @@
|
|
1
|
+
require 'lotus/validations/coercions'
|
2
|
+
|
3
|
+
# Quick fix for non MRI VMs that don't implement Range#size
|
4
|
+
#
|
5
|
+
# @since 0.1.0
|
6
|
+
class Range
|
7
|
+
def size
|
8
|
+
to_a.size
|
9
|
+
end unless instance_methods.include?(:size)
|
10
|
+
end
|
11
|
+
|
12
|
+
module Lotus
|
13
|
+
module Validations
|
14
|
+
# Validator for a single attribute
|
15
|
+
#
|
16
|
+
# @since 0.1.0
|
17
|
+
# @api private
|
18
|
+
class AttributeValidator
|
19
|
+
# Attribute naming convention for "confirmation" validation
|
20
|
+
#
|
21
|
+
# @see Lotus::Validations::AttributeValidator#confirmation
|
22
|
+
#
|
23
|
+
# @since 0.1.0
|
24
|
+
# @api private
|
25
|
+
CONFIRMATION_TEMPLATE = '%{name}_confirmation'.freeze
|
26
|
+
|
27
|
+
# Initialize a validator
|
28
|
+
#
|
29
|
+
# @param validator [Lotus::Validations] an object which included
|
30
|
+
# Lotus::Validations module
|
31
|
+
# @param name [Symbol] the name of the attribute
|
32
|
+
# @param options [Hash] the set of validations for the attribute
|
33
|
+
#
|
34
|
+
# @since 0.1.0
|
35
|
+
# @api private
|
36
|
+
def initialize(validator, name, options)
|
37
|
+
@validator, @name, @options = validator, name, options
|
38
|
+
@value = _attribute(@name)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Validate the attribute
|
42
|
+
#
|
43
|
+
# @return [void]
|
44
|
+
#
|
45
|
+
# @since 0.1.0
|
46
|
+
# @api private
|
47
|
+
def validate!
|
48
|
+
presence
|
49
|
+
acceptance
|
50
|
+
|
51
|
+
_run_validations
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
# Validates presence of the value.
|
56
|
+
# This fails with `nil` and "blank" values.
|
57
|
+
#
|
58
|
+
# An object is blank if it isn't `nil`, but doesn't hold a value.
|
59
|
+
# Empty strings and enumerables are an example.
|
60
|
+
#
|
61
|
+
# @see Lotus::Validations::ClassMethods#attribute
|
62
|
+
# @see Lotus::Validations::AttributeValidator#nil_value?
|
63
|
+
#
|
64
|
+
# @since 0.1.0
|
65
|
+
# @api private
|
66
|
+
def presence
|
67
|
+
_validate(__method__) { !blank_value? }
|
68
|
+
end
|
69
|
+
|
70
|
+
# Validates acceptance of the value.
|
71
|
+
#
|
72
|
+
# This passes if the value is "truthy", it fails if not.
|
73
|
+
#
|
74
|
+
# Truthy examples: `Object.new`, `1`, `"1"`, `true`.
|
75
|
+
# Falsey examples: `nil`, `0`, `"0"`, `false`.
|
76
|
+
#
|
77
|
+
# @see Lotus::Validations::ClassMethods#attribute
|
78
|
+
# @see http://www.rubydoc.info/gems/lotus-utils/Lotus/Utils/Kernel#Boolean-class_method
|
79
|
+
#
|
80
|
+
# @since 0.1.0
|
81
|
+
# @api private
|
82
|
+
def acceptance
|
83
|
+
_validate(__method__) { Lotus::Utils::Kernel.Boolean(@value) }
|
84
|
+
end
|
85
|
+
|
86
|
+
# Validates format of the value.
|
87
|
+
#
|
88
|
+
# Coerces the value to a string and then check if it satisfies the defined
|
89
|
+
# matcher.
|
90
|
+
#
|
91
|
+
# @see Lotus::Validations::ClassMethods#attribute
|
92
|
+
#
|
93
|
+
# @since 0.1.0
|
94
|
+
# @api private
|
95
|
+
def format
|
96
|
+
_validate(__method__) {|matcher| @value.to_s.match(matcher) }
|
97
|
+
end
|
98
|
+
|
99
|
+
# Validates inclusion of the value in the defined collection.
|
100
|
+
#
|
101
|
+
# The collection is an objects which implements `#include?`.
|
102
|
+
#
|
103
|
+
# @see Lotus::Validations::ClassMethods#attribute
|
104
|
+
#
|
105
|
+
# @since 0.1.0
|
106
|
+
# @api private
|
107
|
+
def inclusion
|
108
|
+
_validate(__method__) {|collection| collection.include?(@value) }
|
109
|
+
end
|
110
|
+
|
111
|
+
# Validates exclusion of the value in the defined collection.
|
112
|
+
#
|
113
|
+
# The collection is an objects which implements `#include?`.
|
114
|
+
#
|
115
|
+
# @see Lotus::Validations::ClassMethods#attribute
|
116
|
+
#
|
117
|
+
# @since 0.1.0
|
118
|
+
# @api private
|
119
|
+
def exclusion
|
120
|
+
_validate(__method__) {|collection| !collection.include?(@value) }
|
121
|
+
end
|
122
|
+
|
123
|
+
# Validates confirmation of the value with another corresponding value.
|
124
|
+
#
|
125
|
+
# Given a `:password` attribute, it passes if the corresponding attribute
|
126
|
+
# `:password_confirmation` has the same value.
|
127
|
+
#
|
128
|
+
# @see Lotus::Validations::ClassMethods#attribute
|
129
|
+
# @see Lotus::Validations::AttributeValidator::CONFIRMATION_TEMPLATE
|
130
|
+
#
|
131
|
+
# @since 0.1.0
|
132
|
+
# @api private
|
133
|
+
def confirmation
|
134
|
+
_validate(__method__) do
|
135
|
+
_attribute == _attribute(CONFIRMATION_TEMPLATE % { name: @name })
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Validates if value's size matches the defined quantity.
|
140
|
+
#
|
141
|
+
# The quantity can be a Ruby Numeric:
|
142
|
+
#
|
143
|
+
# * `Integer`
|
144
|
+
# * `Fixnum`
|
145
|
+
# * `Float`
|
146
|
+
# * `BigNum`
|
147
|
+
# * `BigDecimal`
|
148
|
+
# * `Complex`
|
149
|
+
# * `Rational`
|
150
|
+
# * Octal literals
|
151
|
+
# * Hex literals
|
152
|
+
# * `#to_int`
|
153
|
+
#
|
154
|
+
# The quantity can be also any object which implements `#include?`.
|
155
|
+
#
|
156
|
+
# If the quantity is a Numeric, the size of the value MUST be exactly the
|
157
|
+
# same.
|
158
|
+
#
|
159
|
+
# If the quantity is a Range, the size of the value MUST be included.
|
160
|
+
#
|
161
|
+
# The value is an object which implements `#size`.
|
162
|
+
#
|
163
|
+
# @raise [ArgumentError] if the defined quantity isn't a Numeric or a
|
164
|
+
# collection
|
165
|
+
#
|
166
|
+
# @see Lotus::Validations::ClassMethods#attribute
|
167
|
+
#
|
168
|
+
# @since 0.1.0
|
169
|
+
# @api private
|
170
|
+
def size
|
171
|
+
_validate(__method__) do |validator|
|
172
|
+
case validator
|
173
|
+
when Numeric, ->(v) { v.respond_to?(:to_int) }
|
174
|
+
@value.size == validator.to_int
|
175
|
+
when Range
|
176
|
+
validator.include?(@value.size)
|
177
|
+
else
|
178
|
+
raise ArgumentError.new("Size validator must be a number or a range, it was: #{ validator }")
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Coerces the value to the defined type.
|
184
|
+
# Built in types are:
|
185
|
+
#
|
186
|
+
# * `Array`
|
187
|
+
# * `BigDecimal`
|
188
|
+
# * `Boolean`
|
189
|
+
# * `Date`
|
190
|
+
# * `DateTime`
|
191
|
+
# * `Float`
|
192
|
+
# * `Hash`
|
193
|
+
# * `Integer`
|
194
|
+
# * `Pathname`
|
195
|
+
# * `Set`
|
196
|
+
# * `String`
|
197
|
+
# * `Symbol`
|
198
|
+
# * `Time`
|
199
|
+
#
|
200
|
+
# If a user defined class is specified, it can be freely used for coercion
|
201
|
+
# purposes. The only limitation is that the constructor should have
|
202
|
+
# **arity of 1**.
|
203
|
+
#
|
204
|
+
# @raise [TypeError] if the coercion fails
|
205
|
+
# @raise [ArgumentError] if the custom coercer's `#initialize` has a wrong arity.
|
206
|
+
#
|
207
|
+
# @see Lotus::Validations::ClassMethods#attribute
|
208
|
+
# @see Lotus::Validations::Coercions
|
209
|
+
#
|
210
|
+
# @since 0.1.0
|
211
|
+
# @api private
|
212
|
+
def coerce
|
213
|
+
_validate(:type) do |coercer|
|
214
|
+
@value = Lotus::Validations::Coercions.coerce(coercer, @value)
|
215
|
+
_attributes[@name] = @value
|
216
|
+
true
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Checks if the value is `nil`.
|
221
|
+
#
|
222
|
+
# @since 0.1.0
|
223
|
+
# @api private
|
224
|
+
def nil_value?
|
225
|
+
@value.nil?
|
226
|
+
end
|
227
|
+
|
228
|
+
alias_method :skip?, :nil_value?
|
229
|
+
|
230
|
+
# Checks if the value is "blank".
|
231
|
+
#
|
232
|
+
# @see Lotus::Validations::AttributeValidator#presence
|
233
|
+
#
|
234
|
+
# @since 0.1.0
|
235
|
+
# @api private
|
236
|
+
def blank_value?
|
237
|
+
nil_value? || (@value.respond_to?(:empty?) && @value.empty?)
|
238
|
+
end
|
239
|
+
|
240
|
+
# Run the defined validations
|
241
|
+
#
|
242
|
+
# @since 0.1.0
|
243
|
+
# @api private
|
244
|
+
def _run_validations
|
245
|
+
return if skip?
|
246
|
+
|
247
|
+
format
|
248
|
+
coerce
|
249
|
+
inclusion
|
250
|
+
exclusion
|
251
|
+
size
|
252
|
+
confirmation
|
253
|
+
end
|
254
|
+
|
255
|
+
# Reads an attribute from the validator.
|
256
|
+
#
|
257
|
+
# @since 0.1.0
|
258
|
+
# @api private
|
259
|
+
def _attribute(name = @name)
|
260
|
+
_attributes[name.to_sym]
|
261
|
+
end
|
262
|
+
|
263
|
+
# @since 0.1.0
|
264
|
+
# @api private
|
265
|
+
def _attributes
|
266
|
+
@validator.__send__(:attributes)
|
267
|
+
end
|
268
|
+
|
269
|
+
# Run a single validation and collects the results.
|
270
|
+
#
|
271
|
+
# @since 0.1.0
|
272
|
+
# @api private
|
273
|
+
def _validate(validation)
|
274
|
+
if (validator = @options[validation]) && !(yield validator)
|
275
|
+
@validator.errors.add(@name, validation, @options.fetch(validation), @value)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'lotus/utils/kernel'
|
2
|
+
|
3
|
+
module Lotus
|
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 values [Array] of objects to be coerced
|
14
|
+
# @param blk [Proc] an optional block to pass to the custom coercer
|
15
|
+
#
|
16
|
+
# @raise [TypeError] if the coercion fails
|
17
|
+
# @raise [ArgumentError] if the custom coercer's `#initialize` has a wrong arity.
|
18
|
+
#
|
19
|
+
# @since 0.1.0
|
20
|
+
# @api private
|
21
|
+
def self.coerce(coercer, *values, &blk)
|
22
|
+
if ::Lotus::Utils::Kernel.respond_to?(coercer.to_s)
|
23
|
+
::Lotus::Utils::Kernel.__send__(coercer.to_s, *values, &blk)
|
24
|
+
else
|
25
|
+
coercer.new(*values, &blk)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Lotus
|
2
|
+
module Validations
|
3
|
+
# A single validation error for an attribute
|
4
|
+
#
|
5
|
+
# @since 0.1.0
|
6
|
+
class Error
|
7
|
+
# The name of the attribute
|
8
|
+
#
|
9
|
+
# @return [Symbol] the name of the attribute
|
10
|
+
#
|
11
|
+
# @since 0.1.0
|
12
|
+
attr_reader :attribute
|
13
|
+
|
14
|
+
# The name of the validation
|
15
|
+
#
|
16
|
+
# @return [Symbol] the name of the validation
|
17
|
+
#
|
18
|
+
# @since 0.1.0
|
19
|
+
attr_reader :validation
|
20
|
+
|
21
|
+
# The expected value
|
22
|
+
#
|
23
|
+
# @return [Object] the expected value
|
24
|
+
#
|
25
|
+
# @since 0.1.0
|
26
|
+
attr_reader :expected
|
27
|
+
|
28
|
+
# The actual value
|
29
|
+
#
|
30
|
+
# @return [Object] the actual value
|
31
|
+
#
|
32
|
+
# @since 0.1.0
|
33
|
+
attr_reader :actual
|
34
|
+
|
35
|
+
# Initialize a validation error
|
36
|
+
#
|
37
|
+
# @param attribute [Symbol] the name of the attribute
|
38
|
+
# @param validation [Symbol] the name of the validation
|
39
|
+
# @param expected [Object] the expected value
|
40
|
+
# @param actual [Object] the actual value
|
41
|
+
#
|
42
|
+
# @since 0.1.0
|
43
|
+
# @api private
|
44
|
+
def initialize(attribute, validation, expected, actual)
|
45
|
+
@attribute, @validation, @expected, @actual =
|
46
|
+
attribute, validation, expected, actual
|
47
|
+
end
|
48
|
+
|
49
|
+
# Check if self equals to `other`
|
50
|
+
#
|
51
|
+
# @since 0.1.0
|
52
|
+
def ==(other)
|
53
|
+
other.is_a?(self.class) &&
|
54
|
+
other.attribute == attribute &&
|
55
|
+
other.validation == validation &&
|
56
|
+
other.expected == expected &&
|
57
|
+
other.actual == actual
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'lotus/validations/error'
|
2
|
+
|
3
|
+
module Lotus
|
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 Lotus::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 {|h,k| h[k] = [] }
|
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
|
+
def empty?
|
27
|
+
@errors.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns how many validations have failed
|
31
|
+
#
|
32
|
+
# @return [Fixnum] the count
|
33
|
+
#
|
34
|
+
# @since 0.1.0
|
35
|
+
def count
|
36
|
+
errors.count
|
37
|
+
end
|
38
|
+
|
39
|
+
alias_method :size, :count
|
40
|
+
|
41
|
+
# Clears the internal state of the errors
|
42
|
+
#
|
43
|
+
# @since 0.1.0
|
44
|
+
# @api private
|
45
|
+
def clear
|
46
|
+
@errors.clear
|
47
|
+
end
|
48
|
+
|
49
|
+
# Iterate thru the errors and yields the given block
|
50
|
+
#
|
51
|
+
# @param blk [Proc] the given block
|
52
|
+
# @yield [error] a Lotus::Validations::Error
|
53
|
+
#
|
54
|
+
# @see Lotus::Validations::Error
|
55
|
+
#
|
56
|
+
# @since 0.1.0
|
57
|
+
def each(&blk)
|
58
|
+
errors.each(&blk)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Iterate thru the errors, yields the given block and collect the
|
62
|
+
# returning value.
|
63
|
+
#
|
64
|
+
# @param blk [Proc] the given block
|
65
|
+
# @yield [error] a Lotus::Validations::Error
|
66
|
+
#
|
67
|
+
# @see Lotus::Validations::Error
|
68
|
+
#
|
69
|
+
# @since 0.1.0
|
70
|
+
def map(&blk)
|
71
|
+
errors.map(&blk)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Add an error to the set
|
75
|
+
#
|
76
|
+
# @param attribute [Symbol] the name of the attribute
|
77
|
+
# @param validation [Symbol] the name of the validation
|
78
|
+
# @param expected [Object] the expected value
|
79
|
+
# @param actual [Object] the actual value
|
80
|
+
#
|
81
|
+
# @since 0.1.0
|
82
|
+
# @api private
|
83
|
+
def add(attribute, validation, expected, actual)
|
84
|
+
@errors[attribute].push(
|
85
|
+
Error.new(attribute, validation, expected, actual)
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Return the errors for the given attribute
|
90
|
+
#
|
91
|
+
# @param attribute [Symbol] the name of the attribute
|
92
|
+
#
|
93
|
+
# @since 0.1.0
|
94
|
+
def for(attribute)
|
95
|
+
@errors[attribute]
|
96
|
+
end
|
97
|
+
|
98
|
+
# Check if the current set of errors equals to the one who belongs to
|
99
|
+
# `other`.
|
100
|
+
#
|
101
|
+
# @param other [Object] the other term of comparison
|
102
|
+
#
|
103
|
+
# @return [TrueClass,FalseClass] the result of comparison
|
104
|
+
#
|
105
|
+
# @since 0.1.0
|
106
|
+
def ==(other)
|
107
|
+
other.is_a?(self.class) &&
|
108
|
+
other.errors == errors
|
109
|
+
end
|
110
|
+
|
111
|
+
alias_method :eql?, :==
|
112
|
+
|
113
|
+
protected
|
114
|
+
# A flatten set of errors for all the attributes
|
115
|
+
#
|
116
|
+
# @since 0.1.0
|
117
|
+
# @api private
|
118
|
+
def errors
|
119
|
+
@errors.values.flatten
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|