hanami-validations 0.5.0 → 0.6.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.
@@ -8,18 +8,19 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Hanami::Validations::VERSION
9
9
  spec.authors = ['Luca Guidi', 'Trung Lê', 'Alfonso Uceda']
10
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}
11
+ spec.summary = 'Validations mixin for Ruby objects'
12
+ spec.description = 'Validations mixin for Ruby objects and support for Hanami'
13
13
  spec.homepage = 'http://hanamirb.org'
14
14
  spec.license = 'MIT'
15
15
 
16
- spec.files = `git ls-files -- lib/* LICENSE.md README.md CHANGELOG.md hanami-validations.gemspec`.split($/)
16
+ spec.files = `git ls-files -- lib/* LICENSE.md README.md CHANGELOG.md hanami-validations.gemspec`.split($INPUT_RECORD_SEPARATOR)
17
17
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ['lib']
20
- spec.required_ruby_version = '>= 2.0.0'
20
+ spec.required_ruby_version = '>= 2.2.0'
21
21
 
22
- spec.add_dependency 'hanami-utils', '~> 0.7'
22
+ spec.add_dependency 'hanami-utils', '~> 0.8'
23
+ spec.add_dependency 'dry-validation', '~> 0.9'
23
24
 
24
25
  spec.add_development_dependency 'bundler', '~> 1.6'
25
26
  spec.add_development_dependency 'minitest', '~> 5'
@@ -1 +1 @@
1
- require 'hanami/validations'
1
+ require 'hanami/validations' # rubocop:disable Style/FileName
@@ -1,17 +1,36 @@
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'
1
+ require 'dry-validation'
2
+ require 'hanami/utils/class_attribute'
3
+ require 'hanami/validations/namespace'
4
+ require 'hanami/validations/predicates'
5
+ require 'hanami/validations/inline_predicate'
6
+ require 'set'
7
+
8
+ Dry::Validation::Messages::Namespaced.configure do |config|
9
+ config.lookup_paths = config.lookup_paths + %w(
10
+ %{root}.%{rule}.%{predicate}
11
+ ).freeze
12
+ end
9
13
 
10
14
  module Hanami
11
15
  # Hanami::Validations is a set of lightweight validations for Ruby objects.
12
16
  #
13
17
  # @since 0.1.0
18
+ #
19
+ # @example
20
+ # require 'hanami/validations'
21
+ #
22
+ # class Signup
23
+ # include Hanami::Validations
24
+ #
25
+ # validations do
26
+ # # ...
27
+ # end
28
+ # end
14
29
  module Validations
30
+ # @since 0.6.0
31
+ # @api private
32
+ DEFAULT_MESSAGES_ENGINE = :yaml
33
+
15
34
  # Override Ruby's hook for modules.
16
35
  #
17
36
  # @param base [Class] the target action
@@ -20,250 +39,308 @@ module Hanami
20
39
  # @api private
21
40
  #
22
41
  # @see http://www.ruby-doc.org/core/Module.html#method-i-included
42
+ # rubocop:disable Metrics/MethodLength
23
43
  def self.included(base)
24
44
  base.class_eval do
25
45
  extend ClassMethods
26
- include AttributeDefiner
46
+
47
+ include Utils::ClassAttribute
48
+ class_attribute :schema
49
+ class_attribute :_messages
50
+ class_attribute :_messages_path
51
+ class_attribute :_namespace
52
+ class_attribute :_predicates_module
53
+
54
+ class_attribute :_predicates
55
+ self._predicates = Set.new
27
56
  end
28
57
  end
58
+ # rubocop:enable Metrics/MethodLength
29
59
 
30
60
  # Validations DSL
31
61
  #
32
62
  # @since 0.1.0
33
63
  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.
64
+ # Define validation rules from the given block.
37
65
  #
38
- # @param base [Class] the target action
66
+ # @param blk [Proc] validation rules
39
67
  #
40
- # @since 0.2.2
41
- # @api private
68
+ # @since 0.6.0
69
+ #
70
+ # @example
71
+ # require 'hanami/validations'
72
+ #
73
+ # class Signup
74
+ # include Hanami::Validations
75
+ #
76
+ # validations do
77
+ # required(:name).filled
78
+ # end
79
+ # end
42
80
  #
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
81
+ # rubocop:disable Metrics/AbcSize
82
+ def validations(&blk)
83
+ schema_predicates = _predicates_module || __predicates
84
+
85
+ base = _build(predicates: schema_predicates, &_base_rules)
86
+ schema = _build(predicates: schema_predicates, rules: base.rules, &blk)
87
+ schema.configure(&_schema_config)
88
+ schema.configure(&_schema_predicates)
89
+ schema.extend(__messages) unless _predicates.empty?
90
+
91
+ self.schema = schema.new
47
92
  end
93
+ # rubocop:enable Metrics/AbcSize
48
94
 
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.
95
+ # Define an inline predicate
52
96
  #
53
- # @param base [Class] the target action
97
+ # @param name [Symbol] inline predicate name
98
+ # @param message [String] optional error message
99
+ # @param blk [Proc] predicate implementation
54
100
  #
55
- # @since 0.1.0
56
- # @api private
101
+ # @return nil
57
102
  #
58
- # @see http://www.ruby-doc.org/core/Module.html#method-i-included
103
+ # @since 0.6.0
59
104
  #
60
- # @example
105
+ # @example Without Custom Message
61
106
  # require 'hanami/validations'
62
107
  #
63
- # module NameValidations
108
+ # class Signup
64
109
  # include Hanami::Validations
65
110
  #
66
- # attribute :name, presence: true
111
+ # predicate :foo? do |actual|
112
+ # actual == 'foo'
113
+ # end
114
+ #
115
+ # validations do
116
+ # required(:name).filled(:foo?)
117
+ # end
67
118
  # end
68
119
  #
120
+ # result = Signup.new(name: nil).call
121
+ # result.messages # => { :name => ['is invalid'] }
122
+ #
123
+ # @example With Custom Message
124
+ # require 'hanami/validations'
125
+ #
69
126
  # class Signup
70
- # include NameValidations
71
- # end
127
+ # include Hanami::Validations
72
128
  #
73
- # signup = Signup.new(name: '')
74
- # signup.valid? # => false
129
+ # predicate :foo?, message: 'must be foo' do |actual|
130
+ # actual == 'foo'
131
+ # end
75
132
  #
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)
133
+ # validations do
134
+ # required(:name).filled(:foo?)
135
+ # end
136
+ # end
137
+ #
138
+ # result = Signup.new(name: nil).call
139
+ # result.messages # => { :name => ['must be foo'] }
140
+ def predicate(name, message: 'is invalid', &blk)
141
+ _predicates << InlinePredicate.new(name, message, &blk)
86
142
  end
87
143
 
88
- # Define a validation for an existing attribute
144
+ # Assign a set of shared predicates wrapped in a module
145
+ #
146
+ # @param mod [Module] a module with shared predicates
89
147
  #
90
- # @param name [#to_sym] the name of the attribute
91
- # @param options [Hash] set of validations
148
+ # @since 0.6.0
92
149
  #
93
- # @see Hanami::Validations::ClassMethods#validations
150
+ # @see Hanami::Validations::Predicates
94
151
  #
95
- # @example Presence
152
+ # @example
96
153
  # require 'hanami/validations'
97
154
  #
98
- # class Signup
99
- # include Hanami::Validations
155
+ # module MySharedPredicates
156
+ # include Hanami::Validations::Predicates
100
157
  #
101
- # def initialize(attributes = {})
102
- # @name = attributes.fetch(:name)
158
+ # predicate :foo? fo |actual|
159
+ # actual == 'foo'
103
160
  # end
161
+ # end
104
162
  #
105
- # attr_accessor :name
163
+ # class MyValidator
164
+ # include Hanami::Validations
165
+ # predicates MySharedPredicates
106
166
  #
107
- # validates :name, presence: true
167
+ # validations do
168
+ # required(:name).filled(:foo?)
169
+ # end
108
170
  # end
171
+ def predicates(mod)
172
+ self._predicates_module = mod
173
+ end
174
+
175
+ # Define the type of engine for error messages.
176
+ #
177
+ # Accepted values are `:yaml` (default), `:i18n`.
178
+ #
179
+ # @param type [Symbol] the preferred engine
180
+ #
181
+ # @since 0.6.0
109
182
  #
110
- # signup = Signup.new(name: 'Luca')
111
- # signup.valid? # => true
183
+ # @example
184
+ # require 'hanami/validations'
185
+ #
186
+ # class Signup
187
+ # include Hanami::Validations
112
188
  #
113
- # signup = Signup.new(name: nil)
114
- # signup.valid? # => false
115
- def validates(name, options)
116
- validations.add(name, options)
189
+ # messages :i18n
190
+ # end
191
+ def messages(type)
192
+ self._messages = type
117
193
  end
118
194
 
119
- # Set of user defined validations
195
+ # Define the path where to find translation file
120
196
  #
121
- # @return [Hash]
197
+ # @param path [String] path to translation file
122
198
  #
123
- # @since 0.2.2
124
- # @api private
125
- def validations
126
- @validations ||= ValidationSet.new
199
+ # @since 0.6.0
200
+ #
201
+ # @example
202
+ # require 'hanami/validations'
203
+ #
204
+ # class Signup
205
+ # include Hanami::Validations
206
+ #
207
+ # messages :i18n
208
+ # end
209
+ def messages_path(path)
210
+ self._messages_path = path
127
211
  end
128
212
 
129
- # Set of user defined attributes
213
+ # Namespace for error messages.
130
214
  #
131
- # @return [Array<String>]
215
+ # @param name [String] namespace
132
216
  #
133
- # @since 0.2.3
134
- # @api private
135
- def defined_attributes
136
- validations.names.map(&:to_s)
217
+ # @since 0.6.0
218
+ #
219
+ # @example
220
+ # require 'hanami/validations'
221
+ #
222
+ # module MyApp
223
+ # module Validators
224
+ # class Signup
225
+ # include Hanami::Validations
226
+ #
227
+ # namespace 'signup'
228
+ # end
229
+ # end
230
+ # end
231
+ #
232
+ # # Instead of looking for error messages under the `my_app.validator.signup`
233
+ # # namespace, it will look just for `signup`.
234
+ # #
235
+ # # This helps to simplify YAML files where are stored error messages
236
+ def namespace(name = nil)
237
+ if name.nil?
238
+ Namespace.new(_namespace, self)
239
+ else
240
+ self._namespace = name.to_s
241
+ end
137
242
  end
138
243
 
139
244
  private
140
245
 
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
246
+ # @since 0.6.0
247
+ # @api private
248
+ def _build(options = {}, &blk)
249
+ options = { build: false }.merge(options)
250
+ Dry::Validation.__send__(_schema_type, options, &blk)
251
+ end
252
+
253
+ # @since 0.6.0
254
+ # @api private
255
+ def _schema_type
256
+ :Schema
257
+ end
258
+
259
+ # @since 0.6.0
146
260
  # @api private
147
- def transfer_validations_to_base(base)
148
- validations.each do |attribute, options|
149
- base.validates attribute, options
261
+ def _base_rules
262
+ lambda do
150
263
  end
151
264
  end
152
- end
153
265
 
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
266
+ # @since 0.6.0
267
+ # @api private
268
+ def _schema_config
269
+ lambda do |config|
270
+ config.messages = _messages unless _messages.nil?
271
+ config.messages_file = _messages_path unless _messages_path.nil?
272
+ config.namespace = namespace
273
+ end
274
+ end
224
275
 
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
276
+ # @since 0.6.0
277
+ # @api private
278
+ def _schema_predicates
279
+ return if _predicates_module.nil? && _predicates.empty?
280
+
281
+ lambda do |config|
282
+ config.messages = _predicates_module && _predicates_module.messages || DEFAULT_MESSAGES_ENGINE
283
+ config.messages_file = _predicates_module && _predicates_module.messages_path
284
+ end
285
+ end
286
+
287
+ # @since 0.6.0
288
+ # @api private
289
+ def __predicates
290
+ mod = Module.new { include Hanami::Validations::Predicates }
291
+
292
+ _predicates.each do |p|
293
+ mod.module_eval do
294
+ predicate(p.name, &p.to_proc)
295
+ end
296
+ end
297
+
298
+ mod
299
+ end
232
300
 
233
- errors.empty?
301
+ # @since 0.6.0
302
+ # @api private
303
+ # rubocop:disable Metrics/MethodLength
304
+ def __messages
305
+ result = _predicates.each_with_object({}) do |p, ret|
306
+ ret[p.name] = p.message
307
+ end
308
+
309
+ Module.new do
310
+ @@__messages = result # rubocop:disable Style/ClassVars
311
+
312
+ def self.extended(base)
313
+ base.instance_eval do
314
+ def __messages
315
+ Hash[en: { errors: @@__messages }]
316
+ end
317
+ end
318
+ end
319
+
320
+ def messages
321
+ super.merge(__messages)
322
+ end
323
+ end
324
+ end
325
+ # rubocop:enable Metrics/MethodLength
234
326
  end
235
327
 
236
- # Checks if the current data doesn't satisfies the defined validations
328
+ # Initialize a new instance of a validator
237
329
  #
238
- # @return [TrueClass,FalseClass] the result of the validations
330
+ # @param input [#to_h] a set of input data
239
331
  #
240
- # @since 0.3.2
241
- def invalid?
242
- !valid?
332
+ # @since 0.6.0
333
+ def initialize(input = {})
334
+ @input = input.to_h
243
335
  end
244
336
 
245
337
  # Validates the object.
246
338
  #
247
- # @return [Errors]
339
+ # @return [Dry::Validations::Result]
248
340
  #
249
341
  # @since 0.2.4
250
- # @api private
251
- #
252
- # @see Hanami::Attribute#nested
253
342
  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)
343
+ self.class.schema.call(@input)
267
344
  end
268
345
 
269
346
  # Returns a Hash with the defined attributes as symbolized keys, and their
@@ -273,33 +350,7 @@ module Hanami
273
350
  #
274
351
  # @since 0.1.0
275
352
  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
353
+ validate.output
303
354
  end
304
355
  end
305
356
  end