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.
@@ -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