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.
@@ -4,20 +4,24 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'hanami/validations/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "hanami-validations"
7
+ spec.name = 'hanami-validations'
8
8
  spec.version = Hanami::Validations::VERSION
9
- spec.authors = ["Luca Guidi"]
10
- spec.email = ["me@lucaguidi.com"]
9
+ spec.authors = ['Luca Guidi', 'Trung Lê', 'Alfonso Uceda']
10
+ spec.email = ['me@lucaguidi.com', 'trung.le@ruby-journal.com', 'uceda73@gmail.com']
11
+ spec.summary = %q{Validations mixin for Ruby objects}
12
+ spec.description = %q{Validations mixin for Ruby objects and support for Hanami}
13
+ spec.homepage = 'http://hanamirb.org'
14
+ spec.license = 'MIT'
11
15
 
12
- spec.summary = %q{The web, with simplicity}
13
- spec.description = %q{Hanami is a web framework for Ruby}
14
- spec.homepage = "http://hanamirb.org"
16
+ spec.files = `git ls-files -- lib/* LICENSE.md README.md CHANGELOG.md hanami-validations.gemspec`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+ spec.required_ruby_version = '>= 2.0.0'
15
21
 
16
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
- spec.bindir = "exe"
18
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
- spec.require_paths = ["lib"]
22
+ spec.add_dependency 'hanami-utils', '~> 0.7'
20
23
 
21
- spec.add_development_dependency "bundler", "~> 1.11"
22
- spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency 'bundler', '~> 1.6'
25
+ spec.add_development_dependency 'minitest', '~> 5'
26
+ spec.add_development_dependency 'rake', '~> 10'
23
27
  end
@@ -0,0 +1 @@
1
+ require 'hanami/validations'
@@ -1,7 +1,305 @@
1
- require "hanami/validations/version"
1
+ require 'hanami/utils/hash'
2
+ require 'hanami/validations/version'
3
+ require 'hanami/validations/blank_value_checker'
4
+ require 'hanami/validations/attribute_definer'
5
+ require 'hanami/validations/validation_set'
6
+ require 'hanami/validations/validator'
7
+ require 'hanami/validations/attribute'
8
+ require 'hanami/validations/errors'
2
9
 
3
10
  module Hanami
11
+ # Hanami::Validations is a set of lightweight validations for Ruby objects.
12
+ #
13
+ # @since 0.1.0
4
14
  module Validations
5
- # Your code goes here...
15
+ # Override Ruby's hook for modules.
16
+ #
17
+ # @param base [Class] the target action
18
+ #
19
+ # @since 0.1.0
20
+ # @api private
21
+ #
22
+ # @see http://www.ruby-doc.org/core/Module.html#method-i-included
23
+ def self.included(base)
24
+ base.class_eval do
25
+ extend ClassMethods
26
+ include AttributeDefiner
27
+ end
28
+ end
29
+
30
+ # Validations DSL
31
+ #
32
+ # @since 0.1.0
33
+ module ClassMethods
34
+ # Override Ruby's hook for class inheritance. When a class includes
35
+ # Hanami::Validations and it is subclassed, this passes
36
+ # the attributes from the superclass to the subclass.
37
+ #
38
+ # @param base [Class] the target action
39
+ #
40
+ # @since 0.2.2
41
+ # @api private
42
+ #
43
+ # @see http://www.ruby-doc.org/core/Class.html#method-i-inherited
44
+ def inherited(base)
45
+ transfer_validations_to_base(base)
46
+ super
47
+ end
48
+
49
+ # Override Ruby's hook for modules. When a module includes
50
+ # Hanami::Validations and it is included in a class or module, this passes
51
+ # the validations from the module to the base.
52
+ #
53
+ # @param base [Class] the target action
54
+ #
55
+ # @since 0.1.0
56
+ # @api private
57
+ #
58
+ # @see http://www.ruby-doc.org/core/Module.html#method-i-included
59
+ #
60
+ # @example
61
+ # require 'hanami/validations'
62
+ #
63
+ # module NameValidations
64
+ # include Hanami::Validations
65
+ #
66
+ # attribute :name, presence: true
67
+ # end
68
+ #
69
+ # class Signup
70
+ # include NameValidations
71
+ # end
72
+ #
73
+ # signup = Signup.new(name: '')
74
+ # signup.valid? # => false
75
+ #
76
+ # signup = Signup.new(name: 'Luca')
77
+ # signup.valid? # => true
78
+ def included(base)
79
+ base.class_eval do
80
+ include Hanami::Validations
81
+ end
82
+
83
+ super
84
+
85
+ transfer_validations_to_base(base)
86
+ end
87
+
88
+ # Define a validation for an existing attribute
89
+ #
90
+ # @param name [#to_sym] the name of the attribute
91
+ # @param options [Hash] set of validations
92
+ #
93
+ # @see Hanami::Validations::ClassMethods#validations
94
+ #
95
+ # @example Presence
96
+ # require 'hanami/validations'
97
+ #
98
+ # class Signup
99
+ # include Hanami::Validations
100
+ #
101
+ # def initialize(attributes = {})
102
+ # @name = attributes.fetch(:name)
103
+ # end
104
+ #
105
+ # attr_accessor :name
106
+ #
107
+ # validates :name, presence: true
108
+ # end
109
+ #
110
+ # signup = Signup.new(name: 'Luca')
111
+ # signup.valid? # => true
112
+ #
113
+ # signup = Signup.new(name: nil)
114
+ # signup.valid? # => false
115
+ def validates(name, options)
116
+ validations.add(name, options)
117
+ end
118
+
119
+ # Set of user defined validations
120
+ #
121
+ # @return [Hash]
122
+ #
123
+ # @since 0.2.2
124
+ # @api private
125
+ def validations
126
+ @validations ||= ValidationSet.new
127
+ end
128
+
129
+ # Set of user defined attributes
130
+ #
131
+ # @return [Array<String>]
132
+ #
133
+ # @since 0.2.3
134
+ # @api private
135
+ def defined_attributes
136
+ validations.names.map(&:to_s)
137
+ end
138
+
139
+ private
140
+
141
+ # Transfers attributes to a base class
142
+ #
143
+ # @param base [Module] the base class to transfer attributes to
144
+ #
145
+ # @since 0.2.2
146
+ # @api private
147
+ def transfer_validations_to_base(base)
148
+ validations.each do |attribute, options|
149
+ base.validates attribute, options
150
+ end
151
+ end
152
+ end
153
+
154
+ # Validation errors
155
+ #
156
+ # @return [Hanami::Validations::Errors] the set of validation errors
157
+ #
158
+ # @since 0.1.0
159
+ #
160
+ # @see Hanami::Validations::Errors
161
+ #
162
+ # @example Valid attributes
163
+ # require 'hanami/validations'
164
+ #
165
+ # class Signup
166
+ # include Hanami::Validations
167
+ #
168
+ # attribute :email, presence: true, format: /\A(.*)@(.*)\.(.*)\z/
169
+ # end
170
+ #
171
+ # signup = Signup.new(email: 'user@example.org')
172
+ # signup.valid? # => true
173
+ #
174
+ # signup.errors
175
+ # # => #<Hanami::Validations::Errors:0x007fd594ba9228 @errors={}>
176
+ #
177
+ # @example Invalid attributes
178
+ # require 'hanami/validations'
179
+ #
180
+ # class Signup
181
+ # include Hanami::Validations
182
+ #
183
+ # attribute :email, presence: true, format: /\A(.*)@(.*)\.(.*)\z/
184
+ # attribute :age, size: 18..99
185
+ # end
186
+ #
187
+ # signup = Signup.new(email: '', age: 17)
188
+ # signup.valid? # => false
189
+ #
190
+ # signup.errors
191
+ # # => #<Hanami::Validations::Errors:0x007fe00ced9b78
192
+ # # @errors={
193
+ # # :email=>[
194
+ # # #<Hanami::Validations::Error:0x007fe00cee3290 @attribute=:email, @validation=:presence, @expected=true, @actual="">,
195
+ # # #<Hanami::Validations::Error:0x007fe00cee31f0 @attribute=:email, @validation=:format, @expected=/\A(.*)@(.*)\.(.*)\z/, @actual="">
196
+ # # ],
197
+ # # :age=>[
198
+ # # #<Hanami::Validations::Error:0x007fe00cee30d8 @attribute=:age, @validation=:size, @expected=18..99, @actual=17>
199
+ # # ]
200
+ # # }>
201
+ #
202
+ # @example Invalid attributes
203
+ # require 'hanami/validations'
204
+ #
205
+ # class Post
206
+ # include Hanami::Validations
207
+ #
208
+ # attribute :title, presence: true
209
+ # end
210
+ #
211
+ # post = Post.new
212
+ # post.invalid? # => true
213
+ #
214
+ # post.errors
215
+ # # => #<Hanami::Validations::Errors:0x2931522b
216
+ # # @errors={
217
+ # # :title=>[
218
+ # # #<Hanami::Validations::Error:0x662706a7 @actual=nil, @attribute_name="title", @validation=:presence, @expected=true, @namespace=nil, @attribute="title">
219
+ # # ]
220
+ # # }>
221
+ def errors
222
+ @errors ||= Errors.new
223
+ end
224
+
225
+ # Checks if the current data satisfies the defined validations
226
+ #
227
+ # @return [TrueClass,FalseClass] the result of the validations
228
+ #
229
+ # @since 0.1.0
230
+ def valid?
231
+ validate
232
+
233
+ errors.empty?
234
+ end
235
+
236
+ # Checks if the current data doesn't satisfies the defined validations
237
+ #
238
+ # @return [TrueClass,FalseClass] the result of the validations
239
+ #
240
+ # @since 0.3.2
241
+ def invalid?
242
+ !valid?
243
+ end
244
+
245
+ # Validates the object.
246
+ #
247
+ # @return [Errors]
248
+ #
249
+ # @since 0.2.4
250
+ # @api private
251
+ #
252
+ # @see Hanami::Attribute#nested
253
+ def validate
254
+ validator = Validator.new(defined_validations, read_attributes, errors)
255
+ validator.validate
256
+ end
257
+
258
+ # Iterates thru the defined attributes and their values
259
+ #
260
+ # @param blk [Proc] a block
261
+ # @yieldparam attribute [Symbol] the name of the attribute
262
+ # @yieldparam value [Object,nil] the value of the attribute
263
+ #
264
+ # @since 0.2.0
265
+ def each(&blk)
266
+ to_h.each(&blk)
267
+ end
268
+
269
+ # Returns a Hash with the defined attributes as symbolized keys, and their
270
+ # relative values.
271
+ #
272
+ # @return [Hash]
273
+ #
274
+ # @since 0.1.0
275
+ def to_h
276
+ # TODO remove this symbolization when we'll support Ruby 2.2+ only
277
+ Utils::Hash.new(
278
+ @attributes
279
+ ).deep_dup.symbolize!.to_h
280
+ end
281
+
282
+ private
283
+ # The set of user defined validations.
284
+ #
285
+ # @since 0.2.2
286
+ # @api private
287
+ #
288
+ # @see Hanami::Validations::ClassMethods#validations
289
+ def defined_validations
290
+ self.class.__send__(:validations)
291
+ end
292
+
293
+ # Builds a Hash of current attribute values.
294
+ #
295
+ # @since 0.2.2
296
+ # @api private
297
+ def read_attributes
298
+ {}.tap do |attributes|
299
+ defined_validations.each_key do |attribute|
300
+ attributes[attribute] = public_send(attribute)
301
+ end
302
+ end
303
+ end
6
304
  end
7
305
  end
@@ -0,0 +1,252 @@
1
+ require 'hanami/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 Hanami
13
+ module Validations
14
+ # A validable attribute
15
+ #
16
+ # @since 0.1.0
17
+ # @api private
18
+ class Attribute
19
+ # Attribute naming convention for "confirmation" validation
20
+ #
21
+ # @see Hanami::Validations::Attribute#confirmation
22
+ #
23
+ # @since 0.2.0
24
+ # @api private
25
+ CONFIRMATION_TEMPLATE = '%{name}_confirmation'.freeze
26
+
27
+ # Instantiate an attribute
28
+ #
29
+ # @param attributes [Hash] a set of attributes and values coming from the
30
+ # input
31
+ # @param name [Symbol] the name of the attribute
32
+ # @param value [Object,nil] the value coming from the input
33
+ # @param validations [Hash] a set of validation rules
34
+ #
35
+ # @since 0.2.0
36
+ # @api private
37
+ def initialize(attributes, name, value, validations, errors)
38
+ @attributes = attributes
39
+ @name = name
40
+ @value = value
41
+ @validations = validations
42
+ @errors = errors
43
+ end
44
+
45
+ # @api private
46
+ # @since 0.2.0
47
+ def validate
48
+ presence
49
+ acceptance
50
+
51
+ return if skip?
52
+
53
+ format
54
+ inclusion
55
+ exclusion
56
+ size
57
+ confirmation
58
+ nested
59
+
60
+ @errors
61
+ end
62
+
63
+ # @api private
64
+ # @since 0.2.0
65
+ attr_reader :value
66
+
67
+ private
68
+ # Validates presence of the value.
69
+ # This fails with `nil` and "blank" values.
70
+ #
71
+ # An object is blank if it isn't `nil`, but doesn't hold a value.
72
+ # Empty strings and enumerables are an example.
73
+ #
74
+ # @see Hanami::Validations::ClassMethods#attribute
75
+ # @see Hanami::Validations::Attribute#nil_value?
76
+ #
77
+ # @since 0.2.0
78
+ # @api private
79
+ def presence
80
+ _validate(__method__) { !blank_value? }
81
+ end
82
+
83
+ # Validates acceptance of the value.
84
+ #
85
+ # This passes if the value is "truthy", it fails if not.
86
+ #
87
+ # Truthy examples: `Object.new`, `1`, `"1"`, `true`.
88
+ # Falsy examples: `nil`, `0`, `"0"`, `false`, `""`.
89
+ #
90
+ # @see Hanami::Validations::ClassMethods#attribute
91
+ # @see http://www.rubydoc.info/gems/hanami-utils/Hanami/Utils/Kernel#Boolean-class_method
92
+ #
93
+ # @since 0.2.0
94
+ # @api private
95
+ def acceptance
96
+ _validate(__method__) do
97
+ !blank_value? && Hanami::Utils::Kernel.Boolean(@value)
98
+ end
99
+ end
100
+
101
+ # Validates format of the value.
102
+ #
103
+ # Coerces the value to a string and then check if it satisfies the defined
104
+ # matcher.
105
+ #
106
+ # @see Hanami::Validations::ClassMethods#attribute
107
+ #
108
+ # @since 0.2.0
109
+ # @api private
110
+ def format
111
+ _validate(__method__) {|matcher| @value.to_s.match(matcher) } unless blank_value?
112
+ end
113
+
114
+ # Validates inclusion of the value in the defined collection.
115
+ #
116
+ # The collection is an objects which implements `#include?`.
117
+ #
118
+ # @see Hanami::Validations::ClassMethods#attribute
119
+ #
120
+ # @since 0.2.0
121
+ # @api private
122
+ def inclusion
123
+ _validate(__method__) {|collection| collection.include?(value) }
124
+ end
125
+
126
+ # Validates exclusion of the value in the defined collection.
127
+ #
128
+ # The collection is an objects which implements `#include?`.
129
+ #
130
+ # @see Hanami::Validations::ClassMethods#attribute
131
+ #
132
+ # @since 0.2.0
133
+ # @api private
134
+ def exclusion
135
+ _validate(__method__) {|collection| !collection.include?(value) }
136
+ end
137
+
138
+ # Validates confirmation of the value with another corresponding value.
139
+ #
140
+ # Given a `:password` attribute, it passes if the corresponding attribute
141
+ # `:password_confirmation` has the same value.
142
+ #
143
+ # @see Hanami::Validations::ClassMethods#attribute
144
+ # @see Hanami::Validations::Attribute::CONFIRMATION_TEMPLATE
145
+ #
146
+ # @since 0.2.0
147
+ # @api private
148
+ def confirmation
149
+ _validate(__method__) do
150
+ _attribute == _attribute(CONFIRMATION_TEMPLATE % { name: @name })
151
+ end
152
+ end
153
+
154
+ # Validates if value's size matches the defined quantity.
155
+ #
156
+ # The quantity can be a Ruby Numeric:
157
+ #
158
+ # * `Integer`
159
+ # * `Fixnum`
160
+ # * `Float`
161
+ # * `BigNum`
162
+ # * `BigDecimal`
163
+ # * `Complex`
164
+ # * `Rational`
165
+ # * Octal literals
166
+ # * Hex literals
167
+ # * `#to_int`
168
+ #
169
+ # The quantity can be also any object which implements `#include?`.
170
+ #
171
+ # If the quantity is a Numeric, the size of the value MUST be exactly the
172
+ # same.
173
+ #
174
+ # If the quantity is a Range, the size of the value MUST be included.
175
+ #
176
+ # If the attribute's value is blank, the size will not be considered
177
+ #
178
+ # The value is an object which implements `#size`.
179
+ #
180
+ # @raise [ArgumentError] if the defined quantity isn't a Numeric or a
181
+ # collection
182
+ #
183
+ # @see Hanami::Validations::ClassMethods#attribute
184
+ #
185
+ # @since 0.2.0
186
+ # @api private
187
+ def size
188
+ return if blank_value?
189
+
190
+ _validate(__method__) do |validator|
191
+ case validator
192
+ when Numeric, ->(v) { v.respond_to?(:to_int) }
193
+ value.size == validator.to_int
194
+ when Range
195
+ validator.include?(value.size)
196
+ else
197
+ raise ArgumentError.new("Size validator must be a number or a range, it was: #{ validator }")
198
+ end
199
+ end
200
+ end
201
+
202
+ # Validates nested Hanami Validations objects
203
+ #
204
+ # @since 0.2.4
205
+ # @api private
206
+ def nested
207
+ _validate(__method__) do |validator|
208
+ errors = value.validate
209
+ errors.each do |error|
210
+ new_error = Error.new(error.attribute, error.validation, error.expected, error.actual, @name)
211
+ @errors.add new_error.attribute, new_error
212
+ end
213
+ true
214
+ end
215
+ end
216
+
217
+ # @since 0.1.0
218
+ # @api private
219
+ def skip?
220
+ @value.nil?
221
+ end
222
+
223
+ # Checks if the value is "blank".
224
+ #
225
+ # @see Hanami::Validations::BlankValueChecker
226
+ #
227
+ # @since 0.2.0
228
+ # @api private
229
+ def blank_value?
230
+ BlankValueChecker.new(@value).blank_value?
231
+ end
232
+
233
+ # Reads an attribute from the validator.
234
+ #
235
+ # @since 0.2.0
236
+ # @api private
237
+ def _attribute(name = @name)
238
+ @attributes[name.to_sym]
239
+ end
240
+
241
+ # Run a single validation and collects the results.
242
+ #
243
+ # @since 0.2.0
244
+ # @api private
245
+ def _validate(validation)
246
+ if (validator = @validations[validation]) && !(yield validator)
247
+ @errors.add(@name, Error.new(@name, validation, @validations.fetch(validation), @value))
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end