validate-rb 0.1.0.pre → 1.1.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.
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Validate
4
+ module Compare
5
+ module TransformUsing
6
+ def using(&transform_block)
7
+ @transform_block = transform_block
8
+ self
9
+ end
10
+
11
+ def <=>(other)
12
+ return super if @transform_block.nil?
13
+
14
+ super(@transform_block.call(other))
15
+ end
16
+ end
17
+
18
+ class WithAttributes
19
+ include Comparable
20
+ prepend TransformUsing
21
+
22
+ def initialize(attributes)
23
+ @attributes = attributes
24
+ end
25
+
26
+ def <=>(other)
27
+ @attributes.each do |attribute, value|
28
+ result = value <=> other.send(attribute)
29
+ return result unless result.zero?
30
+ end
31
+
32
+ 0
33
+ end
34
+
35
+ def method_missing(symbol, *args)
36
+ return super unless args.empty? && respond_to_missing?(symbol)
37
+
38
+ @attributes[symbol]
39
+ end
40
+
41
+ def respond_to_missing?(attribute, _ = false)
42
+ @attributes.include?(attribute)
43
+ end
44
+
45
+ def to_s
46
+ '<attributes ' + @attributes.map { |attribute, value| "#{attribute}: #{value}"}
47
+ .join(', ') + '>'
48
+ end
49
+ end
50
+
51
+ class ToValue
52
+ include Comparable
53
+ prepend TransformUsing
54
+
55
+ def initialize(value_block)
56
+ @value_block = value_block
57
+ end
58
+
59
+ def <=>(other)
60
+ @value_block.call <=> other
61
+ end
62
+
63
+ def to_s
64
+ '<dynamic value>'
65
+ end
66
+ end
67
+
68
+ module_function
69
+
70
+ def attributes(**attributes)
71
+ WithAttributes.new(attributes)
72
+ end
73
+
74
+ def to(value = nil, &value_block)
75
+ value_block ||= value.is_a?(Proc) ? value : proc { value }
76
+ ToValue.new(value_block)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,231 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Validate
5
+ class Constraint
6
+ class Violation
7
+ attr_reader :value, :path, :constraint
8
+
9
+ def initialize(value, path, constraint)
10
+ @value = value
11
+ @path = path
12
+ @constraint = constraint
13
+ end
14
+
15
+ def message(template = @constraint.message)
16
+ (template % parameters).strip
17
+ end
18
+
19
+ alias to_s message
20
+
21
+ private
22
+
23
+ def parameters
24
+ @parameters ||= Hash.new do |_, key|
25
+ String(instance_eval(key.to_s))
26
+ end
27
+ end
28
+ end
29
+
30
+ class Option
31
+ attr_reader :name
32
+
33
+ def initialize(name, default:, assertion: nil, &assert_block)
34
+ @name = name
35
+ @default = default.is_a?(Proc) ? default : -> { default }
36
+ @assertion = assertion || assert_block && Assertions.create(&assert_block)
37
+ end
38
+
39
+ def replace_default(default)
40
+ Option.new(@name, default: default, assertion: @assertion)
41
+ end
42
+
43
+ def get_or_default(options)
44
+ value = options.delete(@name) { return @default&.call }
45
+ @assertion&.assert(value, message: "invalid option #{@name}")
46
+ value
47
+ end
48
+ end
49
+
50
+ def self.inherited(child)
51
+ child.extend DSL
52
+ end
53
+
54
+ def self.create_class(name, **defaults, &constraint_block)
55
+ Class.new(self) do
56
+ @supported_options = common_options.transform_values do |option|
57
+ defaults.include?(option.name) ? option.replace_default(defaults[option.name]) : option
58
+ end
59
+ include(@constraint_user_methods = Module.new)
60
+ @constraint_user_methods.define_method(:name) { name.to_s }
61
+ class_eval(&constraint_block)
62
+ if instance_variable_defined?(:@supported_options)
63
+ initialize { |**options| options }
64
+ end
65
+ end
66
+ end
67
+
68
+ module DSL
69
+ def constraint_name
70
+ @constraint_name ||= Assertions.create do
71
+ not_nil(message: 'constraint name must not be nil')
72
+ is_a(Symbol, message: 'constraint name must be a Symbol')
73
+ end
74
+ end
75
+ module_function :constraint_name
76
+
77
+ def common_options
78
+ @common_options ||= {
79
+ message: Option.new(:message, default: 'be %{constraint.name}') do
80
+ not_blank
81
+ is_a(String)
82
+ end
83
+ }.freeze
84
+ end
85
+ module_function :common_options
86
+
87
+ def option(
88
+ name,
89
+ default: lambda do
90
+ raise Error::KeyError,
91
+ "option #{name.inspect} is required for #{self.name}"
92
+ end,
93
+ &assert_block
94
+ )
95
+ constraint_name.assert(name)
96
+ if @supported_options.include?(name)
97
+ raise Error::ArgumentError, "duplicate option :#{name}"
98
+ end
99
+
100
+ @supported_options[name] = Option.new(
101
+ name,
102
+ default: default,
103
+ &assert_block
104
+ )
105
+ self
106
+ end
107
+
108
+ def initialize(&initialize_block)
109
+ supported_options = @supported_options
110
+ expects_kwargs = false
111
+ initialize_block.parameters.each do |(kind, name)|
112
+ if %i(keyreq key).include?(kind) && supported_options.include?(name)
113
+ raise Error::ArgumentError,
114
+ "key name #{name}: conflicts with an existing option"
115
+ end
116
+ expects_kwargs = true if kind == :keyrest
117
+ end
118
+
119
+ define_constraint_method(:initialize, initialize_block) do |*args, &block|
120
+ if args.last.is_a?(Hash)
121
+ known_options, kwargs =
122
+ args.pop
123
+ .partition { |k, _| supported_options.include?(k) }
124
+ .map { |h| Hash[h] }
125
+
126
+ if !expects_kwargs && !kwargs.empty?
127
+ args << kwargs
128
+ kwargs = {}
129
+ end
130
+
131
+ if expects_kwargs
132
+ merged_options = {}.merge!(super(*args, **kwargs, &block), known_options)
133
+ else
134
+ args << kwargs unless kwargs.empty?
135
+ merged_options = {}.merge!(super(*args, &block), known_options)
136
+ end
137
+ else
138
+ merged_options = super(*args, &block)
139
+ end
140
+
141
+ options = supported_options.each_with_object({}) do |(n, opt), opts|
142
+ opts[n] = opt.get_or_default(merged_options)
143
+ end
144
+
145
+ unless merged_options.empty?
146
+ raise Error::ArgumentError,
147
+ "unexpected options #{merged_options.inspect}"
148
+ end
149
+
150
+ @options = options.freeze
151
+ end
152
+ remove_instance_variable(:@supported_options)
153
+ end
154
+
155
+ def evaluate(&validation_block)
156
+ define_constraint_method(:valid?, validation_block) do |*args|
157
+ catch(:result) do
158
+ super(*args[0...validation_block.arity])
159
+ :pass
160
+ end == :pass
161
+ end
162
+ self
163
+ end
164
+
165
+ def describe(&describe_block)
166
+ define_method(:to_s, &describe_block)
167
+ self
168
+ end
169
+
170
+ def key(&key_block)
171
+ define_method(:name, &key_block)
172
+ self
173
+ end
174
+
175
+ private
176
+
177
+ def define_constraint_method(name, body, &override)
178
+ @constraint_user_methods.__send__(:define_method, name, &body)
179
+ define_method(name, &override)
180
+ self
181
+ end
182
+ end
183
+
184
+ attr_reader :options
185
+ protected :options
186
+
187
+ def initialize(**options)
188
+ @options = options
189
+ end
190
+
191
+ def name
192
+ raise ::NotImplementedError
193
+ end
194
+
195
+ def valid?(value, ctx = Constraints::ValidationContext.none)
196
+ raise ::NotImplementedError
197
+ end
198
+
199
+ def to_s
200
+ name.to_s.gsub('_', ' ')
201
+ end
202
+
203
+ def inspect
204
+ "#<#{self.class.name} #{@options.map { |name, value| "#{name}: #{value.inspect}" }.join(', ')}>"
205
+ end
206
+
207
+ def ==(other)
208
+ other.is_a?(Constraint) && other.name == name && other.options == options
209
+ end
210
+
211
+ def respond_to_missing?(method, _ = false)
212
+ @options.include?(method)
213
+ end
214
+
215
+ def method_missing(method, *args)
216
+ return super unless args.empty? || respond_to_missing?(method)
217
+
218
+ @options[method]
219
+ end
220
+
221
+ private
222
+
223
+ def fail
224
+ throw(:result, :fail)
225
+ end
226
+
227
+ def pass
228
+ throw(:result, :pass)
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,511 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Validate
4
+ module Constraints
5
+ include Validate::Arguments
6
+
7
+ @reserved_names = Hash[%i[define validation_context].map { |n| [n, n] }]
8
+
9
+ arg(:name) do
10
+ not_blank(message: 'constraint name must not be blank')
11
+ is_a(Symbol, message: 'constraint name must be a Symbol')
12
+ end
13
+ arg(:body) do
14
+ not_nil(message: 'constraint body is required')
15
+ end
16
+ def self.define(name, **defaults, &body)
17
+ if @reserved_names.include?(name)
18
+ raise Error::ArgumentError,
19
+ "#{name} is already defined"
20
+ end
21
+
22
+ @reserved_names[name] = name
23
+ constraint_class = Constraint.create_class(name, **defaults, &body)
24
+ Constraints.const_set(Helpers.camelize(name), constraint_class)
25
+ define_method(name, &constraint_class.method(:new))
26
+ module_function(name)
27
+ end
28
+
29
+ define(:not_nil, message: 'not be nil') do
30
+ evaluate { |value| fail if value.nil? }
31
+ end
32
+
33
+ define(:not_blank, message: 'not be blank') do
34
+ evaluate do |value|
35
+ fail if value.nil? || !value.respond_to?(:empty?) || value.empty?
36
+ end
37
+ end
38
+
39
+ define(:not_empty, message: 'not be empty') do
40
+ evaluate { |value| fail if value&.empty? }
41
+ end
42
+
43
+ define(:is_a, message: 'be a %{constraint.klass}') do
44
+ option(:klass) do
45
+ not_nil(message: 'klass is required')
46
+ end
47
+
48
+ initialize { |klass| { klass: klass } }
49
+ evaluate do |value|
50
+ pass if value.nil?
51
+
52
+ klass = options[:klass]
53
+ fail unless klass === value
54
+ end
55
+ end
56
+
57
+ define(:respond_to, message: 'respond to %{constraint.method_name.inspect}') do
58
+ option(:method_name) do
59
+ not_blank(message: 'method_name is required')
60
+ is_a(Symbol, message: 'method_name must be a Symbol')
61
+ end
62
+
63
+ initialize do |method_name|
64
+ method_name.nil? ? {} : { method_name: method_name }
65
+ end
66
+ evaluate do |instance|
67
+ pass if instance.nil?
68
+ fail unless instance.respond_to?(options[:method_name])
69
+ end
70
+ key { "respond_to_#{options[:method_name]}" }
71
+ end
72
+
73
+ define(:length, message: 'have length of %{constraint.describe_length}') do
74
+ option(:min) { respond_to(:>, message: 'min must respond to :>') }
75
+ option(:max) { respond_to(:<, message: 'max must respond to :<') }
76
+
77
+ initialize do |range = nil|
78
+ case range
79
+ when ::Range
80
+ { min: range.min, max: range.max }
81
+ else
82
+ { min: range, max: range }
83
+ end
84
+ end
85
+ evaluate do |value|
86
+ pass if value.nil?
87
+ fail unless value.respond_to?(:length)
88
+
89
+ length = value.length
90
+ fail if (options[:min]&.> length) || (options[:max]&.< length)
91
+ end
92
+
93
+ def describe_length
94
+ if options[:max] == options[:min]
95
+ options[:max].to_s
96
+ elsif options[:max].nil?
97
+ "at least #{options[:min]}"
98
+ elsif options[:min].nil?
99
+ "at most #{options[:max]}"
100
+ else
101
+ "at least #{options[:min]} and at most #{options[:max]}"
102
+ end
103
+ end
104
+ key { "length_over_#{options[:min]}_under_#{options[:max]}" }
105
+ end
106
+
107
+ define(:bytesize, message: 'have byte length of %{constraint.describe_length}') do
108
+ option(:min) { respond_to(:>, message: 'min must respond to :>') }
109
+ option(:max) { respond_to(:<, message: 'max must respond to :<') }
110
+
111
+ initialize do |range = nil|
112
+ case range
113
+ when ::Range
114
+ { min: range.min, max: range.max }
115
+ else
116
+ { min: range, max: range }
117
+ end
118
+ end
119
+ evaluate do |value|
120
+ pass if value.nil?
121
+ fail unless value.respond_to?(:bytesize)
122
+
123
+ bytesize = value.bytesize
124
+ fail if (options[:min]&.> bytesize) || (options[:max]&.< bytesize)
125
+ end
126
+
127
+ def describe_length
128
+ if options[:max] == options[:min]
129
+ options[:max].to_s
130
+ elsif options[:max].nil?
131
+ "at least #{options[:min]}"
132
+ elsif options[:min].nil?
133
+ "at most #{options[:max]}"
134
+ else
135
+ "at least #{options[:min]} and at most #{options[:max]}"
136
+ end
137
+ end
138
+ key { "bytesize_over_#{options[:min]}_under_#{options[:max]}" }
139
+ end
140
+
141
+ define(:one_of, message: 'be %{constraint.describe_presence}') do
142
+ option(:values) do
143
+ respond_to(:include?, message: 'values must respond to :include?')
144
+ end
145
+
146
+ initialize do |*values|
147
+ if values.one? && values.first.respond_to?(:include?)
148
+ { values: values.first }
149
+ else
150
+ { values: values }
151
+ end
152
+ end
153
+ evaluate do |value|
154
+ pass if value.nil?
155
+
156
+ values = options[:values]
157
+ pass if values.respond_to?(:cover?) && values.cover?(value)
158
+ fail unless values.include?(value)
159
+ end
160
+
161
+ def describe_presence
162
+ case options[:values]
163
+ when ::Hash
164
+ "one of #{options[:values].keys}"
165
+ when ::Range
166
+ "covered by #{options[:values]}"
167
+ else
168
+ "one of #{options[:values]}"
169
+ end
170
+ end
171
+ end
172
+
173
+ define(:validate, message: 'pass validation') do
174
+ option(:using) do
175
+ not_nil(message: 'using is required')
176
+ is_a(Proc, message: 'using must be a Proc')
177
+ end
178
+
179
+ initialize { |&validate_block| { using: validate_block } }
180
+ evaluate do |value|
181
+ pass if value.nil?
182
+ fail unless instance_exec(value, &options[:using])
183
+ end
184
+ end
185
+
186
+ define(:attr, message: 'have attribute %{constraint.attribute}') do
187
+ option(:attribute) do
188
+ not_nil(message: 'attribute is required')
189
+ is_a(Symbol, message: 'attribute must be a Symbol')
190
+ end
191
+ option(:constraints) do
192
+ is_a(AST::DefinitionContext, message: 'constraints must be a DefinitionContext')
193
+ end
194
+
195
+ initialize do |attribute, &block|
196
+ { attribute: attribute,
197
+ constraints: block && AST::DefinitionContext.create(&block) }
198
+ end
199
+ evaluate do |value, ctx|
200
+ pass if value.nil?
201
+
202
+ attribute = options[:attribute]
203
+ begin
204
+ options[:constraints].evaluate(ctx.attr(attribute))
205
+ rescue NameError
206
+ fail
207
+ end
208
+ end
209
+ key { "attr_#{options[:attribute]}" }
210
+ end
211
+
212
+ define(:key, message: 'have key %{constraint.key.inspect}') do
213
+ option(:key) do
214
+ not_nil(message: 'key is required')
215
+ end
216
+ option(:constraints) do
217
+ is_a(AST::DefinitionContext, message: 'constraints must be a DefinitionContext')
218
+ end
219
+
220
+ initialize do |key, &block|
221
+ { key: key,
222
+ constraints: block && AST::DefinitionContext.create(&block) }
223
+ end
224
+
225
+ evaluate do |instance, ctx|
226
+ pass if instance.nil?
227
+ fail unless instance.respond_to?(:[])
228
+
229
+ key = options[:key]
230
+ begin
231
+ options[:constraints]&.evaluate(ctx[key])
232
+ rescue KeyError
233
+ fail
234
+ end
235
+ end
236
+ key { "key_#{options[:key]}" }
237
+ end
238
+
239
+ define(:min, message: 'be at least %{constraint.min}') do
240
+ option(:min) do
241
+ not_nil(message: 'min is required')
242
+ respond_to(:>, message: 'min must respond to :>')
243
+ end
244
+
245
+ initialize do |min = nil|
246
+ min.nil? ? {} : { min: min }
247
+ end
248
+ evaluate do |value|
249
+ pass if value.nil?
250
+ fail if options[:min] > value
251
+ end
252
+ end
253
+
254
+ define(:max, message: 'be at most %{constraint.max}') do
255
+ option(:max) do
256
+ not_nil(message: 'max is required')
257
+ respond_to(:<, message: 'max must respond to :<')
258
+ end
259
+
260
+ initialize do |max = nil|
261
+ max.nil? ? {} : { max: max }
262
+ end
263
+ evaluate do |value|
264
+ pass if value.nil?
265
+ fail if options[:max] < value
266
+ end
267
+ end
268
+
269
+ define(:equal, message: 'be equal to %{constraint.equal}') do
270
+ option(:equal) do
271
+ not_nil(message: 'equal is required')
272
+ respond_to(:==, message: 'equal must respond to :==')
273
+ end
274
+
275
+ initialize do |equal = nil|
276
+ equal.nil? ? {} : { equal: equal }
277
+ end
278
+ evaluate do |value|
279
+ pass if value.nil?
280
+ fail unless options[:equal] == value
281
+ end
282
+ end
283
+
284
+ define(:match, message: 'match %{constraint.regexp}') do
285
+ option(:regexp) do
286
+ not_nil(message: 'regexp is required')
287
+ respond_to(:=~, message: 'regexp must respond to :=~')
288
+ end
289
+
290
+ initialize do |regexp = nil|
291
+ regexp.nil? ? {} : { regexp: regexp }
292
+ end
293
+ evaluate do |value|
294
+ pass if value.nil?
295
+ fail unless value.is_a?(String) && options[:regexp] =~ value
296
+ end
297
+ end
298
+
299
+ define(:valid, message: 'be valid %{constraint.validator || value.class}') do
300
+ option(:validator, default: nil)
301
+
302
+ initialize do |validator = nil|
303
+ validator.nil? ? {} : { validator: validator }
304
+ end
305
+ evaluate do |value, ctx|
306
+ pass if value.nil?
307
+
308
+ Scope.current
309
+ .validator(options[:validator] || value.class)
310
+ .validate(ctx)
311
+ end
312
+ key { options[:validator] && "valid_#{options[:validator]}" || 'valid' }
313
+ end
314
+
315
+ define(:each_value, message: 'have values') do
316
+ option(:constraints) do
317
+ not_nil(message: 'constraints are required')
318
+ is_a(AST::DefinitionContext, message: 'constraints must be a DefinitionContext')
319
+ end
320
+
321
+ initialize do |&block|
322
+ return {} if block.nil?
323
+
324
+ { constraints: AST::DefinitionContext.create(&block) }
325
+ end
326
+ evaluate do |collection, ctx|
327
+ pass if collection.nil?
328
+ fail unless collection.respond_to?(:each)
329
+
330
+ constraints = options[:constraints]
331
+ case collection
332
+ when ::Hash
333
+ collection.each do |key, value|
334
+ constraints.evaluate(ctx[key])
335
+ end
336
+ else
337
+ i = 0
338
+ collection.each do |value|
339
+ constraints.evaluate(ctx[i])
340
+ i += 1
341
+ end
342
+ end
343
+ end
344
+ end
345
+
346
+ define(:each_key, message: 'have keys') do
347
+ option(:constraints) do
348
+ not_nil(message: 'constraints are required')
349
+ is_a(AST::DefinitionContext, message: 'constraints must be a DefinitionContext')
350
+ end
351
+
352
+ initialize do |&block|
353
+ return {} if block.nil?
354
+
355
+ { constraints: AST::DefinitionContext.create(&block) }
356
+ end
357
+ evaluate do |collection, ctx|
358
+ pass if collection.nil?
359
+ fail unless collection.respond_to?(:each_key)
360
+
361
+ constraints = options[:constraints]
362
+ collection.each_key do |key|
363
+ key_ctx = Constraints::ValidationContext.key(key)
364
+ constraints.evaluate(key_ctx)
365
+ ctx.merge(key_ctx) if key_ctx.has_violations?
366
+ end
367
+ end
368
+ end
369
+
370
+ define(:start_with, message: 'start with %{constraint.prefix}') do
371
+ option(:prefix) do
372
+ not_blank(message: 'prefix is required')
373
+ is_a(String, message: 'prefix must be a String')
374
+ end
375
+
376
+ initialize do |prefix = nil|
377
+ return {} if prefix.nil?
378
+
379
+ { prefix: prefix }
380
+ end
381
+ evaluate do |value|
382
+ pass if value.nil?
383
+ fail unless value.respond_to?(:start_with?) && value.start_with?(options[:prefix])
384
+ end
385
+ key do
386
+ "start_with_#{options[:prefix]}"
387
+ end
388
+ end
389
+
390
+ define(:end_with, message: 'end with %{constraint.suffix}') do
391
+ option(:suffix) do
392
+ not_blank(message: 'suffix is required')
393
+ is_a(String, message: 'suffix must be a String')
394
+ end
395
+
396
+ initialize do |suffix = nil|
397
+ return {} if suffix.nil?
398
+
399
+ { suffix: suffix }
400
+ end
401
+ evaluate do |value|
402
+ pass if value.nil?
403
+ fail unless value.respond_to?(:end_with?) && value.end_with?(options[:suffix])
404
+ end
405
+ key do
406
+ "end_with_#{options[:suffix]}"
407
+ end
408
+ end
409
+
410
+ define(:contain, message: 'contain %{constraint.substring}') do
411
+ option(:substring) do
412
+ not_blank(message: 'substring is required')
413
+ is_a(String, message: 'substring must be a String')
414
+ end
415
+
416
+ initialize do |substring = nil|
417
+ return {} if substring.nil?
418
+
419
+ { substring: substring }
420
+ end
421
+ evaluate do |value|
422
+ pass if value.nil?
423
+ fail unless value.respond_to?(:include?) && value.include?(options[:substring])
424
+ end
425
+ key do
426
+ "contain_#{options[:substring]}"
427
+ end
428
+ end
429
+
430
+ define(:uuid, message: 'be a uuid') do
431
+ UUID_REGEXP = %r{\b\h{8}\b-\h{4}-\h{4}-\h{4}-\b\h{12}\b}
432
+
433
+ evaluate do |value|
434
+ pass if value.nil?
435
+ fail unless UUID_REGEXP.match?(value.to_s)
436
+ end
437
+ end
438
+
439
+ define(:hostname, message: 'be a hostname') do
440
+ HOSTNAME_REGEXP = URI::HOST
441
+
442
+ evaluate do |value|
443
+ pass if value.nil?
444
+ fail unless HOSTNAME_REGEXP.match?(value.to_s)
445
+ end
446
+ end
447
+
448
+ define(:uri, message: 'be a uri') do
449
+ option(:absolute, default: true) do
450
+ one_of([true, false], message: ':absolute must be true or false')
451
+ end
452
+
453
+ evaluate do |value|
454
+ pass if value.nil?
455
+ uri = begin
456
+ URI.parse(value)
457
+ rescue URI::Error
458
+ fail
459
+ end
460
+ fail unless options[:absolute] == uri.absolute?
461
+ end
462
+ end
463
+
464
+ define(:ip_address, message: 'be an ip address') do
465
+ option(:version, default: nil) do
466
+ one_of([:v4, :v6], message: 'must be a valid ip version')
467
+ end
468
+
469
+ initialize do |version = nil|
470
+ return {} if version.nil?
471
+
472
+ { version: version }
473
+ end
474
+ evaluate do |value|
475
+ pass if value.nil?
476
+ addr = begin
477
+ IPAddr.new(value)
478
+ rescue IPAddr::Error
479
+ fail
480
+ end
481
+ version = options[:version]
482
+ fail unless version.nil? || addr.send(:"ip#{version}?")
483
+ end
484
+ end
485
+
486
+ define(
487
+ :unique,
488
+ message: 'have unique %{constraint.describe_unique_attribute}'
489
+ ) do
490
+ option(:attribute, default: nil) do
491
+ is_a(Symbol, message: 'attribute %{value.inspect} must be a Symbol')
492
+ end
493
+
494
+ initialize do |attribute = nil|
495
+ attribute.nil? ? {} : { attribute: attribute }
496
+ end
497
+ evaluate do |value|
498
+ pass if value.nil?
499
+ fail unless value.respond_to?(:uniq) && value.respond_to?(:size)
500
+ fail unless value.size == value.uniq(&options[:attribute]).size
501
+ end
502
+ key do
503
+ options[:attribute] && "unique_#{options[:attribute]}" || 'unique'
504
+ end
505
+
506
+ def describe_unique_attribute
507
+ options[:attribute] || 'values'
508
+ end
509
+ end
510
+ end
511
+ end