validate-rb 0.1.0.pre → 1.1.0

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