validate-rb 0.1.0.alpha.1 → 0.1.0.pre

Sign up to get free protection for your applications and to get access to all the features.
data/lib/validate/ast.rb DELETED
@@ -1,381 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- module Validate
5
- module AST
6
- def self.build(*args, &block)
7
- Generator.new
8
- .generate(*args, &block)
9
- .freeze
10
- end
11
-
12
- class DefinitionContext
13
- module Builder
14
- module_function
15
-
16
- def all_constraints(*constraints)
17
- Rules::Unanimous.new(constraints.map { |node| send(*node) })
18
- end
19
-
20
- def at_least_one_constraint(*constraints)
21
- Rules::Affirmative.new(constraints.map { |node| send(*node) })
22
- end
23
-
24
- def no_constraints(*constraints)
25
- Rules::Negative.new(constraints.map { |node| send(*node) })
26
- end
27
-
28
- def constraint(name, args, block, trace)
29
- if defined?(Constraints) && Constraints.respond_to?(name)
30
- begin
31
- return Constraints.send(name, *(args.map { |node| send(*node) }), &block)
32
- rescue => e
33
- ::Kernel.raise Error::ValidationRuleError, e.message, trace
34
- end
35
- end
36
-
37
- Rules::Pending.new(name, args.map { |node| send(*node) }, block, trace)
38
- end
39
-
40
- def value(value)
41
- value
42
- end
43
- end
44
-
45
- def self.create(*args, &block)
46
- ast = AST.build(*args, &block)
47
- context = new
48
- ast.each { |node| context.add_constraint(Builder.send(*node)) }
49
- context
50
- end
51
-
52
- def initialize
53
- @constraints = {}
54
- end
55
-
56
- def add_constraint(constraint)
57
- if @constraints.include?(constraint.name)
58
- raise Error::ValidationRuleError,
59
- "duplicate constraint #{constraint.name}"
60
- end
61
-
62
- @constraints[constraint.name] = constraint
63
- self
64
- end
65
-
66
- def evaluate(ctx)
67
- @constraints.each_value
68
- .reject { |c| catch(:pending) { c.valid?(ctx.value, ctx) } }
69
- .each { |c| ctx.add_violation(c) }
70
- ctx
71
- end
72
- end
73
-
74
- CORE_CONSTRAINTS = %i[
75
- not_nil not_blank not_empty
76
- is_a one_of validate
77
- min max equal match
78
- valid each_value unique
79
- length
80
- ].freeze
81
-
82
- class Generator < ::BasicObject
83
- def initialize
84
- @stack = []
85
- end
86
-
87
- def generate(*args, &block)
88
- instance_exec(*args, &block)
89
-
90
- if @stack.one? && @stack.first[0] == :all_constraints
91
- return @stack.first[1..-1]
92
- end
93
-
94
- @stack
95
- end
96
-
97
- def &(other)
98
- unless other == self
99
- ::Kernel.raise(
100
- Error::ValidationRuleError,
101
- 'bad rule, only constraints and &, |, and ! operators allowed'
102
- )
103
- end
104
-
105
- right = @stack.pop
106
- left = @stack.pop
107
- if right[0] == :all_constraints
108
- right.insert(1, left)
109
- @stack << right
110
- else
111
- @stack << [:all_constraints, left, right]
112
- end
113
- self
114
- end
115
-
116
- def |(other)
117
- unless other == self
118
- ::Kernel.raise(
119
- Error::ValidationRuleError,
120
- 'bad rule, only constraints and &, |, and ! operators allowed'
121
- )
122
- end
123
-
124
- right = @stack.pop
125
- left = @stack.pop
126
- if right[0] == :at_least_one_constraint
127
- right.insert(1, left)
128
- @stack << right
129
- else
130
- @stack << [:at_least_one_constraint, left, right]
131
- end
132
- self
133
- end
134
-
135
- def !
136
- prev = @stack.pop
137
- if prev[0] == :no_constraints
138
- @stack << prev[1]
139
- elsif prev[0] == :all_constraints
140
- prev[0] = :no_constraints
141
- @stack << prev
142
- else
143
- @stack << [:no_constraints, prev]
144
- end
145
- self
146
- end
147
-
148
- private
149
-
150
- def method_missing(method, *args, &block)
151
- return super unless respond_to_missing?(method)
152
-
153
- @stack << [
154
- :constraint,
155
- method,
156
- args.map { |arg| [:value, arg] },
157
- block,
158
- ::Kernel.caller
159
- .reject { |line| line.include?(__FILE__) }
160
- ]
161
- self
162
- end
163
-
164
- def respond_to_missing?(method, _ = false)
165
- (defined?(Constraints) && Constraints.respond_to?(method)) || CORE_CONSTRAINTS.include?(method)
166
- end
167
- end
168
-
169
- module Combinator
170
- extend Forwardable
171
- def_delegators :@constraints, :[]
172
-
173
- def respond_to_missing?(_, _ = false)
174
- false
175
- end
176
-
177
- private
178
-
179
- def constraint_message(index)
180
- @constraints[index].message % Hash.new do |_, key|
181
- if key.to_s.start_with?('constraint')
182
- "%{#{key.to_s.gsub('constraint', "constraint[#{index}]")}}"
183
- else
184
- "%{#{key}}"
185
- end
186
- end
187
- end
188
- end
189
-
190
- module Rules
191
- class Pending < Constraint
192
- include MonitorMixin
193
-
194
- def initialize(name, args, block, caller)
195
- @name = name
196
- @args = args
197
- @block = block
198
- @caller = caller
199
- @constraint = nil
200
-
201
- extend SingleForwardable
202
- mon_initialize
203
- end
204
-
205
- def name
206
- load_constraint { return @name }.name
207
- end
208
-
209
- def valid?(value, ctx = Constraints::ValidationContext.none)
210
- load_constraint { throw(:pending, true) }.valid?(value, ctx)
211
- end
212
-
213
- def to_s
214
- load_constraint { return "[pending #{@name}]" }.to_s
215
- end
216
-
217
- def inspect
218
- load_constraint { return "[pending #{@name}]" }.inspect
219
- end
220
-
221
- def ==(other)
222
- load_constraint { return false } == other
223
- end
224
-
225
- def method_missing(method, *args)
226
- load_constraint { return NameError }.__send__(method, *args)
227
- end
228
-
229
- def respond_to_missing?(method, pvt = false)
230
- load_constraint { return false }.__send__(:respond_to_missing?, method, pvt)
231
- end
232
-
233
- private
234
-
235
- def load_constraint
236
- yield unless defined?(Constraints) && Constraints.respond_to?(@name)
237
-
238
- synchronize do
239
- return @constraint if @constraint
240
-
241
- begin
242
- @constraint = Constraints.send(@name, *@args, &@block)
243
- rescue => e
244
- ::Kernel.raise Error::ValidationRuleError, e.message, @caller
245
- end
246
-
247
- def_delegators(:@constraint, :name, :valid?, :to_s,
248
- :inspect, :==, :message)
249
-
250
- @name = @args = @block = @caller = nil
251
- @constraint
252
- end
253
- end
254
- end
255
-
256
- class Unanimous < Constraint
257
- include Combinator
258
- include Arguments
259
-
260
- arg(:constraints) do
261
- not_nil
262
- length(min: 2)
263
- each_value { is_a(Constraint) }
264
- unique(:name)
265
- end
266
- def initialize(constraints)
267
- @constraints = constraints.freeze
268
- end
269
-
270
- def valid?(value, _ = Constraints::ValidationContext.none)
271
- ctx = Constraints::ValidationContext.root(value)
272
- @constraints.all? do |c|
273
- c.valid?(value, ctx) && !ctx.has_violations?
274
- end
275
- end
276
-
277
- def name
278
- 'both_' + @constraints.map(&:name).sort.join('_and_')
279
- end
280
-
281
- def inspect
282
- return @constraints.first.inspect if @constraints.one?
283
-
284
- "(#{@constraints.map(&:inspect).join(' & ')})"
285
- end
286
-
287
- def message
288
- 'both ' + @constraints
289
- .size
290
- .times
291
- .map { |i| "[#{constraint_message(i)}]" }
292
- .join(', and ')
293
- end
294
- end
295
-
296
- class Affirmative < Constraint
297
- include Combinator
298
- include Arguments
299
-
300
- arg(:constraints) do
301
- not_nil
302
- length(min: 2)
303
- each_value { is_a(Constraint) }
304
- unique(:name)
305
- end
306
- def initialize(constraints)
307
- @constraints = constraints.freeze
308
- end
309
-
310
- def valid?(value, _ = Constraints::ValidationContext.none)
311
- ctx = Constraints::ValidationContext.root(value)
312
- @constraints.any? do |c|
313
- ctx.clear_violations
314
- c.valid?(value, ctx) && !ctx.has_violations?
315
- end
316
- end
317
-
318
- def name
319
- 'either_' + @constraints.map(&:name).sort.join('_or_')
320
- end
321
-
322
- def inspect
323
- return @constraints.first.inspect if @constraints.one?
324
-
325
- "(#{@constraints.map(&:inspect).join(' | ')})"
326
- end
327
-
328
- def message
329
- 'either ' + @constraints
330
- .size
331
- .times
332
- .map { |i| "[#{constraint_message(i)}]" }
333
- .join(', or ')
334
- end
335
- end
336
-
337
- class Negative < Constraint
338
- include Combinator
339
- include Arguments
340
-
341
- arg(:constraints) do
342
- not_nil
343
- not_empty
344
- each_value { is_a(Constraint) }
345
- unique(:name)
346
- end
347
- def initialize(constraints)
348
- @constraints = constraints.freeze
349
- end
350
-
351
- def valid?(value, _ = Constraints::ValidationContext.none)
352
- ctx = Constraints::ValidationContext.root(value)
353
- @constraints.none? do |c|
354
- ctx.clear_violations
355
- c.valid?(value, ctx) && !ctx.has_violations?
356
- end
357
- end
358
-
359
- def name
360
- 'neither_' + @constraints.map(&:name).sort.join('_nor_')
361
- end
362
-
363
- def message
364
- return "not [#{constraint_message(0)}]" if @constraints.one?
365
-
366
- 'neither ' + @constraints
367
- .size
368
- .times
369
- .map { |i| "[#{constraint_message(i)}]" }
370
- .join(', nor ')
371
- end
372
-
373
- def inspect
374
- return "!#{@constraints.first.inspect}" if @constraints.one?
375
-
376
- "!(#{@constraints.map(&:inspect).join(' & ')})"
377
- end
378
- end
379
- end
380
- end
381
- end
@@ -1,34 +0,0 @@
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
@@ -1,217 +0,0 @@
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