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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +89 -0
- data/LICENSE.md +22 -0
- data/README.md +559 -7
- data/hanami-validations.gemspec +16 -12
- data/lib/hanami-validations.rb +1 -0
- data/lib/hanami/validations.rb +300 -2
- data/lib/hanami/validations/attribute.rb +252 -0
- data/lib/hanami/validations/attribute_definer.rb +526 -0
- data/lib/hanami/validations/blank_value_checker.rb +55 -0
- data/lib/hanami/validations/coercions.rb +31 -0
- data/lib/hanami/validations/error.rb +95 -0
- data/lib/hanami/validations/errors.rb +155 -0
- data/lib/hanami/validations/nested_attributes.rb +22 -0
- data/lib/hanami/validations/validation_set.rb +81 -0
- data/lib/hanami/validations/validator.rb +29 -0
- data/lib/hanami/validations/version.rb +2 -1
- metadata +57 -16
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/bin/console +0 -14
- data/bin/setup +0 -8
@@ -0,0 +1,526 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'hanami/utils/attributes'
|
3
|
+
require 'hanami/validations/nested_attributes'
|
4
|
+
|
5
|
+
module Hanami
|
6
|
+
module Validations
|
7
|
+
# Define attributes and their validations together
|
8
|
+
#
|
9
|
+
# @since 0.2.2
|
10
|
+
# @api private
|
11
|
+
module AttributeDefiner
|
12
|
+
# @since 0.2.3
|
13
|
+
# @api private
|
14
|
+
HANAMI_ENTITY_CLASS_NAME = 'Hanami::Entity'.freeze
|
15
|
+
|
16
|
+
# @since 0.2.3
|
17
|
+
# @api private
|
18
|
+
HANAMI_ENTITY_ID = 'id'.freeze
|
19
|
+
|
20
|
+
# Override Ruby's hook for modules.
|
21
|
+
#
|
22
|
+
# @param base [Class] the target class
|
23
|
+
#
|
24
|
+
# @since 0.2.2
|
25
|
+
# @api private
|
26
|
+
#
|
27
|
+
# @see http://www.ruby-doc.org/core/Module.html#method-i-included
|
28
|
+
def self.included(base)
|
29
|
+
base.extend ClassMethods
|
30
|
+
base.extend EntityAttributeDefiner if hanami_entity?(base)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Decide if enable the support for `Hanami::Entity`.
|
34
|
+
#
|
35
|
+
# @param base [Class]
|
36
|
+
#
|
37
|
+
# @since 0.2.3
|
38
|
+
# @api private
|
39
|
+
def self.hanami_entity?(base)
|
40
|
+
base.included_modules.any? do |m|
|
41
|
+
m.to_s == HANAMI_ENTITY_CLASS_NAME
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# @since 0.2.2
|
46
|
+
# @api private
|
47
|
+
module ClassMethods
|
48
|
+
# Override Ruby's hook for modules.
|
49
|
+
#
|
50
|
+
# @param base [Class] the target class
|
51
|
+
#
|
52
|
+
# @since 0.2.2
|
53
|
+
# @api private
|
54
|
+
#
|
55
|
+
# @see http://www.ruby-doc.org/core/Module.html#method-i-extended
|
56
|
+
def included(base)
|
57
|
+
super
|
58
|
+
base.defined_attributes.merge(defined_attributes)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Override Ruby's hook for modules.
|
62
|
+
#
|
63
|
+
# @param base [Class] the target class
|
64
|
+
#
|
65
|
+
# @since 0.2.2
|
66
|
+
# @api private
|
67
|
+
#
|
68
|
+
# @see http://www.ruby-doc.org/core/Module.html#method-i-extended
|
69
|
+
def inherited(base)
|
70
|
+
super
|
71
|
+
base.defined_attributes.merge(defined_attributes)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Define an attribute
|
75
|
+
#
|
76
|
+
# @param name [#to_sym] the name of the attribute
|
77
|
+
# @param options [Hash] optional set of validations
|
78
|
+
# @option options [Class] :type the Ruby type used to coerce the value
|
79
|
+
# @option options [TrueClass,FalseClass] :acceptance requires a non-empty
|
80
|
+
# value which coerces to TrueClass
|
81
|
+
# @option options [TrueClass,FalseClass] :confirmation requires the value
|
82
|
+
# to be confirmed twice
|
83
|
+
# @option options [#include?] :exclusion requires the value NOT be
|
84
|
+
# included in the given collection
|
85
|
+
# @option options [Regexp] :format requires value to match the given
|
86
|
+
# Regexp
|
87
|
+
# @option options [#include?] :inclusion requires the value BE included in
|
88
|
+
# the given collection
|
89
|
+
# @option options [TrueClass,FalseClass] :presence requires the value be
|
90
|
+
# included in the given collection
|
91
|
+
# @option options [Numeric,Range] :size requires value's to be equal or
|
92
|
+
# included by the given validator
|
93
|
+
#
|
94
|
+
# @raise [ArgumentError] if an unknown or mispelled validation is given
|
95
|
+
#
|
96
|
+
# @since 0.2.2
|
97
|
+
#
|
98
|
+
# @example Attributes
|
99
|
+
# require 'hanami/validations'
|
100
|
+
#
|
101
|
+
# class Person
|
102
|
+
# include Hanami::Validations
|
103
|
+
#
|
104
|
+
# attribute :name
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
# person = Person.new(name: 'Luca', age: 32)
|
108
|
+
# person.name # => "Luca"
|
109
|
+
# person.age # => raises NoMethodError because `:age` wasn't defined as attribute.
|
110
|
+
#
|
111
|
+
# @example Standard coercions
|
112
|
+
# require 'hanami/validations'
|
113
|
+
#
|
114
|
+
# class Person
|
115
|
+
# include Hanami::Validations
|
116
|
+
#
|
117
|
+
# attribute :fav_number, type: Integer
|
118
|
+
# end
|
119
|
+
#
|
120
|
+
# person = Person.new(fav_number: '23')
|
121
|
+
# person.valid?
|
122
|
+
#
|
123
|
+
# person.fav_number # => 23
|
124
|
+
#
|
125
|
+
# @example Custom coercions
|
126
|
+
# require 'hanami/validations'
|
127
|
+
#
|
128
|
+
# class FavNumber
|
129
|
+
# def initialize(number)
|
130
|
+
# @number = number
|
131
|
+
# end
|
132
|
+
# end
|
133
|
+
#
|
134
|
+
# class BirthDate
|
135
|
+
# end
|
136
|
+
#
|
137
|
+
# class Person
|
138
|
+
# include Hanami::Validations
|
139
|
+
#
|
140
|
+
# attribute :fav_number, type: FavNumber
|
141
|
+
# attribute :date, type: BirthDate
|
142
|
+
# end
|
143
|
+
#
|
144
|
+
# person = Person.new(fav_number: '23', date: 'Oct 23, 2014')
|
145
|
+
# person.valid?
|
146
|
+
#
|
147
|
+
# person.fav_number # => 23
|
148
|
+
# person.date # => this raises an error, because BirthDate#initialize doesn't accept any arg
|
149
|
+
#
|
150
|
+
# @example Acceptance
|
151
|
+
# require 'hanami/validations'
|
152
|
+
#
|
153
|
+
# class Signup
|
154
|
+
# include Hanami::Validations
|
155
|
+
#
|
156
|
+
# attribute :terms_of_service, acceptance: true
|
157
|
+
# end
|
158
|
+
#
|
159
|
+
# signup = Signup.new(terms_of_service: '1')
|
160
|
+
# signup.valid? # => true
|
161
|
+
#
|
162
|
+
# signup = Signup.new(terms_of_service: '')
|
163
|
+
# signup.valid? # => false
|
164
|
+
#
|
165
|
+
# @example Confirmation
|
166
|
+
# require 'hanami/validations'
|
167
|
+
#
|
168
|
+
# class Signup
|
169
|
+
# include Hanami::Validations
|
170
|
+
#
|
171
|
+
# attribute :password, confirmation: true
|
172
|
+
# end
|
173
|
+
#
|
174
|
+
# signup = Signup.new(password: 'secret', password_confirmation: 'secret')
|
175
|
+
# signup.valid? # => true
|
176
|
+
#
|
177
|
+
# signup = Signup.new(password: 'secret', password_confirmation: 'x')
|
178
|
+
# signup.valid? # => false
|
179
|
+
#
|
180
|
+
# @example Exclusion
|
181
|
+
# require 'hanami/validations'
|
182
|
+
#
|
183
|
+
# class Signup
|
184
|
+
# include Hanami::Validations
|
185
|
+
#
|
186
|
+
# attribute :music, exclusion: ['pop']
|
187
|
+
# end
|
188
|
+
#
|
189
|
+
# signup = Signup.new(music: 'rock')
|
190
|
+
# signup.valid? # => true
|
191
|
+
#
|
192
|
+
# signup = Signup.new(music: 'pop')
|
193
|
+
# signup.valid? # => false
|
194
|
+
#
|
195
|
+
# @example Format
|
196
|
+
# require 'hanami/validations'
|
197
|
+
#
|
198
|
+
# class Signup
|
199
|
+
# include Hanami::Validations
|
200
|
+
#
|
201
|
+
# attribute :name, format: /\A[a-zA-Z]+\z/
|
202
|
+
# end
|
203
|
+
#
|
204
|
+
# signup = Signup.new(name: 'Luca')
|
205
|
+
# signup.valid? # => true
|
206
|
+
#
|
207
|
+
# signup = Signup.new(name: '23')
|
208
|
+
# signup.valid? # => false
|
209
|
+
#
|
210
|
+
# @example Inclusion
|
211
|
+
# require 'hanami/validations'
|
212
|
+
#
|
213
|
+
# class Signup
|
214
|
+
# include Hanami::Validations
|
215
|
+
#
|
216
|
+
# attribute :age, inclusion: 18..99
|
217
|
+
# end
|
218
|
+
#
|
219
|
+
# signup = Signup.new(age: 32)
|
220
|
+
# signup.valid? # => true
|
221
|
+
#
|
222
|
+
# signup = Signup.new(age: 17)
|
223
|
+
# signup.valid? # => false
|
224
|
+
#
|
225
|
+
# @example Presence
|
226
|
+
# require 'hanami/validations'
|
227
|
+
#
|
228
|
+
# class Signup
|
229
|
+
# include Hanami::Validations
|
230
|
+
#
|
231
|
+
# attribute :name, presence: true
|
232
|
+
# end
|
233
|
+
#
|
234
|
+
# signup = Signup.new(name: 'Luca')
|
235
|
+
# signup.valid? # => true
|
236
|
+
#
|
237
|
+
# signup = Signup.new(name: nil)
|
238
|
+
# signup.valid? # => false
|
239
|
+
#
|
240
|
+
# @example Size
|
241
|
+
# require 'hanami/validations'
|
242
|
+
#
|
243
|
+
# class Signup
|
244
|
+
# MEGABYTE = 1024 ** 2
|
245
|
+
# include Hanami::Validations
|
246
|
+
#
|
247
|
+
# attribute :ssn, size: 11 # exact match
|
248
|
+
# attribute :password, size: 8..64 # range
|
249
|
+
# attribute :avatar, size 1..(5 * MEGABYTE)
|
250
|
+
# end
|
251
|
+
#
|
252
|
+
# signup = Signup.new(password: 'a-very-long-password')
|
253
|
+
# signup.valid? # => true
|
254
|
+
#
|
255
|
+
# signup = Signup.new(password: 'short')
|
256
|
+
# signup.valid? # => false
|
257
|
+
def attribute(name, options = {}, &block)
|
258
|
+
_attribute(name, options, &block)
|
259
|
+
end
|
260
|
+
|
261
|
+
# Define an attribute
|
262
|
+
#
|
263
|
+
# @see Hanami::Validations::AttributeDefiner#attribute
|
264
|
+
#
|
265
|
+
# @since 0.3.1
|
266
|
+
# @api private
|
267
|
+
def _attribute(name, options = {}, &block)
|
268
|
+
if block_given?
|
269
|
+
define_nested_attribute(name, options, &block)
|
270
|
+
validates(name, {})
|
271
|
+
else
|
272
|
+
define_attribute(name, options)
|
273
|
+
validates(name, options)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# Set of user defined attributes
|
278
|
+
#
|
279
|
+
# @return [Array<String>]
|
280
|
+
#
|
281
|
+
# @since 0.2.2
|
282
|
+
# @api private
|
283
|
+
def defined_attributes
|
284
|
+
@defined_attributes ||= Set.new(super)
|
285
|
+
end
|
286
|
+
|
287
|
+
private
|
288
|
+
|
289
|
+
# @since 0.2.2
|
290
|
+
# @api private
|
291
|
+
def define_attribute(name, options)
|
292
|
+
type = options.fetch(:type) { nil }
|
293
|
+
|
294
|
+
define_accessor(name, type)
|
295
|
+
defined_attributes.add(name.to_s)
|
296
|
+
|
297
|
+
if options[:confirmation]
|
298
|
+
confirmation_accessor = "#{ name }_confirmation"
|
299
|
+
define_accessor(confirmation_accessor, type)
|
300
|
+
defined_attributes.add(confirmation_accessor)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
# @since 0.2.4
|
305
|
+
# @api private
|
306
|
+
def define_nested_attribute(name, options, &block)
|
307
|
+
nested_class = build_validation_class(&block)
|
308
|
+
define_lazy_reader(name, nested_class)
|
309
|
+
define_coerced_writer(name, nested_class)
|
310
|
+
defined_attributes.add(name.to_s)
|
311
|
+
validates(name, nested: true)
|
312
|
+
end
|
313
|
+
|
314
|
+
# @since 0.2.2
|
315
|
+
# @api private
|
316
|
+
def define_accessor(name, type)
|
317
|
+
if type
|
318
|
+
define_reader(name)
|
319
|
+
define_coerced_writer(name, type)
|
320
|
+
else
|
321
|
+
define_reader(name)
|
322
|
+
define_writer(name)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
# @since 0.2.2
|
327
|
+
# @api private
|
328
|
+
def define_coerced_writer(name, type)
|
329
|
+
define_method("#{ name }=") do |value|
|
330
|
+
@attributes.set(name, Hanami::Validations::Coercions.coerce(type, value))
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
# @since 0.2.2
|
335
|
+
# @api private
|
336
|
+
def define_writer(name)
|
337
|
+
define_method("#{ name }=") do |value|
|
338
|
+
@attributes.set(name, value)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
# @since 0.2.2
|
343
|
+
# @api private
|
344
|
+
def define_reader(name)
|
345
|
+
define_method(name) do
|
346
|
+
@attributes.get(name)
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
# Defines a reader that will return a new instance of
|
351
|
+
# the given type if one is not already present
|
352
|
+
#
|
353
|
+
# @since 0.2.4
|
354
|
+
# @api private
|
355
|
+
def define_lazy_reader(name, type)
|
356
|
+
define_method(name) do
|
357
|
+
value = @attributes.get(name)
|
358
|
+
return value if value
|
359
|
+
|
360
|
+
type.new({}).tap do |val|
|
361
|
+
@attributes.set(name, val)
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
# Creates a validation class and configures it with the
|
367
|
+
# given block.
|
368
|
+
#
|
369
|
+
# @since 0.2.4
|
370
|
+
# @api private
|
371
|
+
def build_validation_class(&blk)
|
372
|
+
NestedAttributes.fabricate(&blk)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
# Support for `Hanami::Entity`
|
377
|
+
#
|
378
|
+
# @since 0.2.3
|
379
|
+
# @api private
|
380
|
+
#
|
381
|
+
# @example
|
382
|
+
# require 'hanami/model'
|
383
|
+
# require 'hanami/validations'
|
384
|
+
#
|
385
|
+
# class Product
|
386
|
+
# include Hanami::Entity
|
387
|
+
# include Hanami::Validations
|
388
|
+
#
|
389
|
+
# attribute :name, type: String, presence: true
|
390
|
+
# attribute :price, type: Integer, presence: true
|
391
|
+
# end
|
392
|
+
#
|
393
|
+
# product = Product.new(name: 'Computer', price: '100')
|
394
|
+
#
|
395
|
+
# product.name # => "Computer"
|
396
|
+
# product.price # => 100
|
397
|
+
# product.valid? # => true
|
398
|
+
module EntityAttributeDefiner
|
399
|
+
# Override for Module#extend
|
400
|
+
#
|
401
|
+
# @since 0.2.3
|
402
|
+
# @api private
|
403
|
+
#
|
404
|
+
# @see http://ruby-doc.org/Module.html#method-i-extended
|
405
|
+
def self.extended(base)
|
406
|
+
base.class_eval do
|
407
|
+
include EntityAttributeDefiner::InstanceMethods
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
# @return [Array<String>]
|
412
|
+
#
|
413
|
+
# @since 0.3.1
|
414
|
+
# @api private
|
415
|
+
def defined_attributes
|
416
|
+
super
|
417
|
+
@defined_attributes.merge(attributes.map(&:to_s))
|
418
|
+
end
|
419
|
+
|
420
|
+
# Override attribute accessors function.
|
421
|
+
#
|
422
|
+
# @since 0.3.1
|
423
|
+
#
|
424
|
+
# @api private
|
425
|
+
# @see Hanami::Model::Entity#define_attr_accessor
|
426
|
+
def define_attr_accessor(attr)
|
427
|
+
_attribute(attr)
|
428
|
+
super
|
429
|
+
end
|
430
|
+
|
431
|
+
# @since 0.2.3
|
432
|
+
# @api private
|
433
|
+
#
|
434
|
+
# @see Hanami::Validations::AttributeDefiner#attribute
|
435
|
+
def attribute(name, options = {})
|
436
|
+
attributes name
|
437
|
+
super
|
438
|
+
end
|
439
|
+
|
440
|
+
# @since 0.2.3
|
441
|
+
# @api private
|
442
|
+
#
|
443
|
+
# @see Hanami::Validations::ClassMethods#validates
|
444
|
+
def validates(name, options = {})
|
445
|
+
super
|
446
|
+
define_attribute(name, options)
|
447
|
+
end
|
448
|
+
|
449
|
+
# @since 0.2.3
|
450
|
+
# @api private
|
451
|
+
module InstanceMethods
|
452
|
+
private
|
453
|
+
# @since 0.2.3
|
454
|
+
# @api private
|
455
|
+
#
|
456
|
+
# @see Hanami::Validations::AttributeDefiner#assign_attribute?
|
457
|
+
def assign_attribute?(attr)
|
458
|
+
super || attr.to_s == HANAMI_ENTITY_ID
|
459
|
+
end
|
460
|
+
|
461
|
+
def initialize(attributes = {})
|
462
|
+
super
|
463
|
+
@attributes.set(HANAMI_ENTITY_ID, id)
|
464
|
+
self.class.attribute(HANAMI_ENTITY_ID)
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
# Create a new instance with the given attributes
|
470
|
+
#
|
471
|
+
# @param attributes [#to_h] an Hash like object which contains the
|
472
|
+
# attributes
|
473
|
+
#
|
474
|
+
# @since 0.2.2
|
475
|
+
#
|
476
|
+
# @example Initialize with Hash
|
477
|
+
# require 'hanami/validations'
|
478
|
+
#
|
479
|
+
# class Signup
|
480
|
+
# include Hanami::Validations
|
481
|
+
#
|
482
|
+
# attribute :name
|
483
|
+
# end
|
484
|
+
#
|
485
|
+
# signup = Signup.new(name: 'Luca')
|
486
|
+
#
|
487
|
+
# @example Initialize with Hash like
|
488
|
+
# require 'hanami/validations'
|
489
|
+
#
|
490
|
+
# class Params
|
491
|
+
# def initialize(attributes)
|
492
|
+
# @attributes = Hash[*attributes]
|
493
|
+
# end
|
494
|
+
#
|
495
|
+
# def to_h
|
496
|
+
# @attributes.to_h
|
497
|
+
# end
|
498
|
+
# end
|
499
|
+
#
|
500
|
+
# class Signup
|
501
|
+
# include Hanami::Validations
|
502
|
+
#
|
503
|
+
# attribute :name
|
504
|
+
# end
|
505
|
+
#
|
506
|
+
# params = Params.new([:name, 'Luca'])
|
507
|
+
# signup = Signup.new(params)
|
508
|
+
#
|
509
|
+
# signup.name # => "Luca"
|
510
|
+
def initialize(attributes = {})
|
511
|
+
@attributes ||= Utils::Attributes.new
|
512
|
+
|
513
|
+
attributes.to_h.each do |key, value|
|
514
|
+
public_send("#{ key }=", value) if assign_attribute?(key)
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
private
|
519
|
+
# @since 0.2.2
|
520
|
+
# @api private
|
521
|
+
def assign_attribute?(attr)
|
522
|
+
self.class.defined_attributes.include?(attr.to_s)
|
523
|
+
end
|
524
|
+
end
|
525
|
+
end
|
526
|
+
end
|