lotus-validations 0.0.0 → 0.1.0

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