hanami-validations 0.5.0 → 0.6.0

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