hanami-validations 0.0.0 → 0.5.0

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