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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +537 -326
- data/hanami-validations.gemspec +6 -5
- data/lib/hanami-validations.rb +1 -1
- data/lib/hanami/validations.rb +259 -208
- data/lib/hanami/validations/form.rb +47 -0
- data/lib/hanami/validations/inline_predicate.rb +46 -0
- data/lib/hanami/validations/namespace.rb +65 -0
- data/lib/hanami/validations/predicates.rb +45 -0
- data/lib/hanami/validations/version.rb +1 -1
- metadata +24 -16
- data/lib/hanami/validations/attribute.rb +0 -252
- data/lib/hanami/validations/attribute_definer.rb +0 -526
- data/lib/hanami/validations/blank_value_checker.rb +0 -55
- data/lib/hanami/validations/coercions.rb +0 -31
- data/lib/hanami/validations/error.rb +0 -95
- data/lib/hanami/validations/errors.rb +0 -155
- data/lib/hanami/validations/nested_attributes.rb +0 -22
- data/lib/hanami/validations/validation_set.rb +0 -81
- data/lib/hanami/validations/validator.rb +0 -29
data/hanami-validations.gemspec
CHANGED
@@ -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 =
|
12
|
-
spec.description =
|
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.
|
20
|
+
spec.required_ruby_version = '>= 2.2.0'
|
21
21
|
|
22
|
-
spec.add_dependency 'hanami-utils',
|
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'
|
data/lib/hanami-validations.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
require 'hanami/validations'
|
1
|
+
require 'hanami/validations' # rubocop:disable Style/FileName
|
data/lib/hanami/validations.rb
CHANGED
@@ -1,17 +1,36 @@
|
|
1
|
-
require '
|
2
|
-
require 'hanami/
|
3
|
-
require 'hanami/validations/
|
4
|
-
require 'hanami/validations/
|
5
|
-
require 'hanami/validations/
|
6
|
-
require '
|
7
|
-
|
8
|
-
|
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
|
-
|
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
|
-
#
|
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
|
66
|
+
# @param blk [Proc] validation rules
|
39
67
|
#
|
40
|
-
# @since 0.
|
41
|
-
#
|
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
|
-
#
|
44
|
-
def
|
45
|
-
|
46
|
-
|
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
|
-
#
|
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
|
97
|
+
# @param name [Symbol] inline predicate name
|
98
|
+
# @param message [String] optional error message
|
99
|
+
# @param blk [Proc] predicate implementation
|
54
100
|
#
|
55
|
-
# @
|
56
|
-
# @api private
|
101
|
+
# @return nil
|
57
102
|
#
|
58
|
-
# @
|
103
|
+
# @since 0.6.0
|
59
104
|
#
|
60
|
-
# @example
|
105
|
+
# @example Without Custom Message
|
61
106
|
# require 'hanami/validations'
|
62
107
|
#
|
63
|
-
#
|
108
|
+
# class Signup
|
64
109
|
# include Hanami::Validations
|
65
110
|
#
|
66
|
-
#
|
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
|
71
|
-
# end
|
127
|
+
# include Hanami::Validations
|
72
128
|
#
|
73
|
-
#
|
74
|
-
#
|
129
|
+
# predicate :foo?, message: 'must be foo' do |actual|
|
130
|
+
# actual == 'foo'
|
131
|
+
# end
|
75
132
|
#
|
76
|
-
#
|
77
|
-
#
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
-
#
|
144
|
+
# Assign a set of shared predicates wrapped in a module
|
145
|
+
#
|
146
|
+
# @param mod [Module] a module with shared predicates
|
89
147
|
#
|
90
|
-
# @
|
91
|
-
# @param options [Hash] set of validations
|
148
|
+
# @since 0.6.0
|
92
149
|
#
|
93
|
-
# @see Hanami::Validations::
|
150
|
+
# @see Hanami::Validations::Predicates
|
94
151
|
#
|
95
|
-
# @example
|
152
|
+
# @example
|
96
153
|
# require 'hanami/validations'
|
97
154
|
#
|
98
|
-
#
|
99
|
-
# include Hanami::Validations
|
155
|
+
# module MySharedPredicates
|
156
|
+
# include Hanami::Validations::Predicates
|
100
157
|
#
|
101
|
-
#
|
102
|
-
#
|
158
|
+
# predicate :foo? fo |actual|
|
159
|
+
# actual == 'foo'
|
103
160
|
# end
|
161
|
+
# end
|
104
162
|
#
|
105
|
-
#
|
163
|
+
# class MyValidator
|
164
|
+
# include Hanami::Validations
|
165
|
+
# predicates MySharedPredicates
|
106
166
|
#
|
107
|
-
#
|
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
|
-
#
|
111
|
-
#
|
183
|
+
# @example
|
184
|
+
# require 'hanami/validations'
|
185
|
+
#
|
186
|
+
# class Signup
|
187
|
+
# include Hanami::Validations
|
112
188
|
#
|
113
|
-
#
|
114
|
-
#
|
115
|
-
def
|
116
|
-
|
189
|
+
# messages :i18n
|
190
|
+
# end
|
191
|
+
def messages(type)
|
192
|
+
self._messages = type
|
117
193
|
end
|
118
194
|
|
119
|
-
#
|
195
|
+
# Define the path where to find translation file
|
120
196
|
#
|
121
|
-
# @
|
197
|
+
# @param path [String] path to translation file
|
122
198
|
#
|
123
|
-
# @since 0.
|
124
|
-
#
|
125
|
-
|
126
|
-
|
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
|
-
#
|
213
|
+
# Namespace for error messages.
|
130
214
|
#
|
131
|
-
# @
|
215
|
+
# @param name [String] namespace
|
132
216
|
#
|
133
|
-
# @since 0.
|
134
|
-
#
|
135
|
-
|
136
|
-
|
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
|
-
#
|
142
|
-
#
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
148
|
-
|
149
|
-
base.validates attribute, options
|
261
|
+
def _base_rules
|
262
|
+
lambda do
|
150
263
|
end
|
151
264
|
end
|
152
|
-
end
|
153
265
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
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
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
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
|
-
|
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
|
-
#
|
328
|
+
# Initialize a new instance of a validator
|
237
329
|
#
|
238
|
-
# @
|
330
|
+
# @param input [#to_h] a set of input data
|
239
331
|
#
|
240
|
-
# @since 0.
|
241
|
-
def
|
242
|
-
|
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 [
|
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
|
-
|
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
|
-
|
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
|