validate-rb 0.1.0.pre → 1.0.0.alpha.1
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/LICENSE +21 -0
- data/README.md +4 -4
- data/lib/validate.rb +75 -1
- data/lib/validate/arguments.rb +93 -0
- data/lib/validate/assertions.rb +27 -0
- data/lib/validate/ast.rb +381 -0
- data/lib/validate/compare.rb +79 -0
- data/lib/validate/constraint.rb +231 -0
- data/lib/validate/constraints.rb +337 -0
- data/lib/validate/constraints/validation_context.rb +167 -0
- data/lib/validate/errors.rb +40 -0
- data/lib/validate/helpers.rb +11 -0
- data/lib/validate/scope.rb +48 -0
- data/lib/validate/validators.rb +7 -0
- data/lib/validate/validators/dsl.rb +44 -0
- data/lib/validate/version.rb +1 -1
- metadata +122 -21
- data/.gitignore +0 -12
- data/.rspec +0 -3
- data/.ruby-version +0 -1
- data/.travis.yml +0 -6
- data/Gemfile +0 -7
- data/Rakefile +0 -6
- data/bin/console +0 -7
- data/bin/setup +0 -6
- data/lib/validate-rb.rb +0 -3
- data/validate-rb.gemspec +0 -30
@@ -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,337 @@
|
|
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(:one_of, message: 'be %{constraint.describe_presence}') do
|
108
|
+
option(:values) do
|
109
|
+
respond_to(:include?, message: 'values must respond to :include?')
|
110
|
+
end
|
111
|
+
|
112
|
+
initialize do |*values|
|
113
|
+
if values.one? && values.first.respond_to?(:include?)
|
114
|
+
{ values: values.first }
|
115
|
+
else
|
116
|
+
{ values: values }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
evaluate do |value|
|
120
|
+
pass if value.nil?
|
121
|
+
|
122
|
+
values = options[:values]
|
123
|
+
pass if values.respond_to?(:cover?) && values.cover?(value)
|
124
|
+
fail unless values.include?(value)
|
125
|
+
end
|
126
|
+
|
127
|
+
def describe_presence
|
128
|
+
case options[:values]
|
129
|
+
when ::Hash
|
130
|
+
"one of #{options[:values].keys}"
|
131
|
+
when ::Range
|
132
|
+
"covered by #{options[:values]}"
|
133
|
+
else
|
134
|
+
"one of #{options[:values]}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
define(:validate, message: 'pass validation') do
|
140
|
+
option(:using) do
|
141
|
+
not_nil(message: 'using is required')
|
142
|
+
is_a(Proc, message: 'using must be a Proc')
|
143
|
+
end
|
144
|
+
|
145
|
+
initialize { |&validate_block| { using: validate_block } }
|
146
|
+
evaluate do |value|
|
147
|
+
pass if value.nil?
|
148
|
+
fail unless instance_exec(value, &options[:using])
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
define(:attr, message: 'have attribute %{constraint.attribute}') do
|
153
|
+
option(:attribute) do
|
154
|
+
not_nil(message: 'attribute is required')
|
155
|
+
is_a(Symbol, message: 'attribute must be a Symbol')
|
156
|
+
end
|
157
|
+
option(:constraints) do
|
158
|
+
is_a(AST::DefinitionContext, message: 'constraints must be a DefinitionContext')
|
159
|
+
end
|
160
|
+
|
161
|
+
initialize do |attribute, &block|
|
162
|
+
{ attribute: attribute,
|
163
|
+
constraints: block && AST::DefinitionContext.create(&block) }
|
164
|
+
end
|
165
|
+
evaluate do |value, ctx|
|
166
|
+
pass if value.nil?
|
167
|
+
|
168
|
+
attribute = options[:attribute]
|
169
|
+
begin
|
170
|
+
options[:constraints].evaluate(ctx.attr(attribute))
|
171
|
+
rescue NameError
|
172
|
+
fail
|
173
|
+
end
|
174
|
+
end
|
175
|
+
key { "attr_#{options[:attribute]}" }
|
176
|
+
end
|
177
|
+
|
178
|
+
define(:key, message: 'have key %{constraint.key.inspect}') do
|
179
|
+
option(:key) do
|
180
|
+
not_nil(message: 'key is required')
|
181
|
+
end
|
182
|
+
option(:constraints) do
|
183
|
+
is_a(AST::DefinitionContext, message: 'constraints must be a DefinitionContext')
|
184
|
+
end
|
185
|
+
|
186
|
+
initialize do |key, &block|
|
187
|
+
{ key: key,
|
188
|
+
constraints: block && AST::DefinitionContext.create(&block) }
|
189
|
+
end
|
190
|
+
|
191
|
+
evaluate do |instance, ctx|
|
192
|
+
pass if instance.nil?
|
193
|
+
fail unless instance.respond_to?(:[])
|
194
|
+
|
195
|
+
key = options[:key]
|
196
|
+
begin
|
197
|
+
options[:constraints]&.evaluate(ctx[key])
|
198
|
+
rescue KeyError
|
199
|
+
fail
|
200
|
+
end
|
201
|
+
end
|
202
|
+
key { "key_#{options[:key]}" }
|
203
|
+
end
|
204
|
+
|
205
|
+
define(:min, message: 'be at least %{constraint.min}') do
|
206
|
+
option(:min) do
|
207
|
+
not_nil(message: 'min is required')
|
208
|
+
respond_to(:>, message: 'min must respond to :>')
|
209
|
+
end
|
210
|
+
|
211
|
+
initialize do |min = nil|
|
212
|
+
min.nil? ? {} : { min: min }
|
213
|
+
end
|
214
|
+
evaluate do |value|
|
215
|
+
pass if value.nil?
|
216
|
+
fail if options[:min] > value
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
define(:max, message: 'be at most %{constraint.max}') do
|
221
|
+
option(:max) do
|
222
|
+
not_nil(message: 'max is required')
|
223
|
+
respond_to(:<, message: 'max must respond to :<')
|
224
|
+
end
|
225
|
+
|
226
|
+
initialize do |max = nil|
|
227
|
+
max.nil? ? {} : { max: max }
|
228
|
+
end
|
229
|
+
evaluate do |value|
|
230
|
+
pass if value.nil?
|
231
|
+
fail if options[:max] < value
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
define(:equal, message: 'be equal to %{constraint.equal}') do
|
236
|
+
option(:equal) do
|
237
|
+
not_nil(message: 'equal is required')
|
238
|
+
respond_to(:==, message: 'equal must respond to :==')
|
239
|
+
end
|
240
|
+
|
241
|
+
initialize do |equal = nil|
|
242
|
+
equal.nil? ? {} : { equal: equal }
|
243
|
+
end
|
244
|
+
evaluate do |value|
|
245
|
+
pass if value.nil?
|
246
|
+
fail unless options[:equal] == value
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
define(:match, message: 'match %{constraint.regexp}') do
|
251
|
+
option(:regexp) do
|
252
|
+
not_nil(message: 'regexp is required')
|
253
|
+
respond_to(:=~, message: 'regexp must respond to :=~')
|
254
|
+
end
|
255
|
+
|
256
|
+
initialize do |regexp = nil|
|
257
|
+
regexp.nil? ? {} : { regexp: regexp }
|
258
|
+
end
|
259
|
+
evaluate do |value|
|
260
|
+
pass if value.nil?
|
261
|
+
fail unless value.is_a?(String) && options[:regexp] =~ value
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
define(:valid, message: 'be valid %{constraint.validator || value.class}') do
|
266
|
+
option(:validator, default: nil)
|
267
|
+
|
268
|
+
initialize do |validator = nil|
|
269
|
+
validator.nil? ? {} : { validator: validator }
|
270
|
+
end
|
271
|
+
evaluate do |value, ctx|
|
272
|
+
pass if value.nil?
|
273
|
+
|
274
|
+
Scope.current
|
275
|
+
.validator(options[:validator] || value.class)
|
276
|
+
.validate(ctx)
|
277
|
+
end
|
278
|
+
key { options[:validator] && "valid_#{options[:validator]}" || 'valid' }
|
279
|
+
end
|
280
|
+
|
281
|
+
define(:each_value, message: 'have values') do
|
282
|
+
option(:constraints) do
|
283
|
+
not_nil(message: 'constraints are required')
|
284
|
+
is_a(AST::DefinitionContext, message: 'constraints must be a DefinitionContext')
|
285
|
+
end
|
286
|
+
|
287
|
+
initialize do |&block|
|
288
|
+
return {} if block.nil?
|
289
|
+
|
290
|
+
{ constraints: AST::DefinitionContext.create(&block) }
|
291
|
+
end
|
292
|
+
evaluate do |collection, ctx|
|
293
|
+
pass if collection.nil?
|
294
|
+
fail unless collection.respond_to?(:each)
|
295
|
+
|
296
|
+
constraints = options[:constraints]
|
297
|
+
case collection
|
298
|
+
when ::Hash
|
299
|
+
collection.each do |key, value|
|
300
|
+
constraints.evaluate(ctx[key])
|
301
|
+
end
|
302
|
+
else
|
303
|
+
i = 0
|
304
|
+
collection.each do |value|
|
305
|
+
constraints.evaluate(ctx[i])
|
306
|
+
i += 1
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
define(
|
313
|
+
:unique,
|
314
|
+
message: 'have unique %{constraint.describe_unique_attribute}'
|
315
|
+
) do
|
316
|
+
option(:attribute, default: nil) do
|
317
|
+
is_a(Symbol, message: 'attribute %{value.inspect} must be a Symbol')
|
318
|
+
end
|
319
|
+
|
320
|
+
initialize do |attribute = nil|
|
321
|
+
attribute.nil? ? {} : { attribute: attribute }
|
322
|
+
end
|
323
|
+
evaluate do |value|
|
324
|
+
pass if value.nil?
|
325
|
+
fail unless value.respond_to?(:uniq) && value.respond_to?(:size)
|
326
|
+
fail unless value.size == value.uniq(&options[:attribute]).size
|
327
|
+
end
|
328
|
+
key do
|
329
|
+
options[:attribute] && "unique_#{options[:attribute]}" || 'unique'
|
330
|
+
end
|
331
|
+
|
332
|
+
def describe_unique_attribute
|
333
|
+
options[:attribute] || 'values'
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|