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

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.
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