validate-rb 0.1.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Validate
4
+ module Compare
5
+ class ByAttributes
6
+ include Comparable
7
+
8
+ def initialize(attributes)
9
+ @attributes = attributes
10
+ end
11
+
12
+ def <=>(other)
13
+ @attributes.map { |attribute, value| value <=> other.send(attribute) }
14
+ .find { |result| !result.zero? } || 0
15
+ end
16
+
17
+ def method_missing(symbol, *args)
18
+ return super unless args.empty? && respond_to_missing?(symbol)
19
+
20
+ @attributes[symbol]
21
+ end
22
+
23
+ def respond_to_missing?(attribute, _ = false)
24
+ @attributes.include?(attribute)
25
+ end
26
+ end
27
+
28
+ module_function
29
+
30
+ def attributes(**attributes)
31
+ ByAttributes.new(attributes)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,217 @@
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, **kwargs, &block|
120
+ known_options, extra_kwargs =
121
+ kwargs.partition { |k, _| supported_options.include?(k) }
122
+ .map { |h| Hash[h] }
123
+ args << extra_kwargs if expects_kwargs || !extra_kwargs.empty?
124
+
125
+ merged_options = {}.merge!(super(*args, &block), known_options)
126
+
127
+ options = supported_options.each_with_object({}) do |(n, opt), opts|
128
+ opts[n] = opt.get_or_default(merged_options)
129
+ end
130
+
131
+ unless merged_options.empty?
132
+ raise Error::ArgumentError,
133
+ "undefined options #{merged_options.inspect}"
134
+ end
135
+
136
+ @options = options.freeze
137
+ end
138
+ remove_instance_variable(:@supported_options)
139
+ end
140
+
141
+ def evaluate(&validation_block)
142
+ define_constraint_method(:valid?, validation_block) do |*args|
143
+ catch(:result) do
144
+ super(*args[0...validation_block.arity])
145
+ :pass
146
+ end == :pass
147
+ end
148
+ self
149
+ end
150
+
151
+ def describe(&describe_block)
152
+ define_method(:to_s, &describe_block)
153
+ self
154
+ end
155
+
156
+ def key(&key_block)
157
+ define_method(:name, &key_block)
158
+ self
159
+ end
160
+
161
+ private
162
+
163
+ def define_constraint_method(name, body, &override)
164
+ @constraint_user_methods.__send__(:define_method, name, &body)
165
+ define_method(name, &override)
166
+ self
167
+ end
168
+ end
169
+
170
+ attr_reader :options
171
+ protected :options
172
+
173
+ def initialize(**options)
174
+ @options = options
175
+ end
176
+
177
+ def name
178
+ raise ::NotImplementedError
179
+ end
180
+
181
+ def valid?(value, ctx = Constraints::ValidationContext.none)
182
+ raise ::NotImplementedError
183
+ end
184
+
185
+ def to_s
186
+ name.to_s.gsub('_', ' ')
187
+ end
188
+
189
+ def inspect
190
+ "#<#{self.class.name} #{@options.map { |name, value| "#{name}: #{value.inspect}" }.join(', ')}>"
191
+ end
192
+
193
+ def ==(other)
194
+ other.is_a?(Constraint) && other.name == name && other.options == options
195
+ end
196
+
197
+ def respond_to_missing?(method, _ = false)
198
+ @options.include?(method)
199
+ end
200
+
201
+ def method_missing(method, *args)
202
+ return super unless args.empty? || respond_to_missing?(method)
203
+
204
+ @options[method]
205
+ end
206
+
207
+ private
208
+
209
+ def fail
210
+ throw(:result, :fail)
211
+ end
212
+
213
+ def pass
214
+ throw(:result, :pass)
215
+ end
216
+ end
217
+ 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