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.
@@ -1,526 +0,0 @@
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