validate-rb 0.1.0.alpha.1 → 1.0.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec9996734d51898a4f003a56c2cb45748c14d58b138ddb03d1291d9cf6a4ed28
4
- data.tar.gz: 144941aa76f0db9224309d7b3614bc41e5e5fd59b37b7419f4785dd77498abf6
3
+ metadata.gz: 2bac2b6ec6ca8d71becc3849cc26401c63a7939c2d9ee7618ccc022f5845080a
4
+ data.tar.gz: 995c743d5e16aa7a4239d0cd4e6eb500b6cd1541184eab44b7605c4f61494751
5
5
  SHA512:
6
- metadata.gz: 3145f4f69e4eb2969d4eafc9c4f92faa4c3a63c2b15061bef8abea109fab30648ace5104c8e8c81314c7b63a1ee0452899d0074cb1ee8fef15ec000443a95f87
7
- data.tar.gz: 7c2f705b8a0da5e05652491060462a783554429a5ea32ecc08e25364ec675e31aac769f23a4f6ec9fbca07e5317cfa9224e99a06820c051b60a8e761890beaea
6
+ metadata.gz: 80bcb6b3ef21a26dd8d39f0dd4666b1185c65011a9dc3cc4e57af6cb9f6329abe7dc9a83fd8aa415e2490d920f7dbdd2b1e950c9838b93b879ca655888944ff0
7
+ data.tar.gz: d687a04ae9a0c0b8a40f3ec4f851583b0edbe2014003f25196b7463beded4c970d6060b0aa79b767e3bb84d46c777103410de5331292220ce026cdb5c08d9b22
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'forwardable'
4
4
  require 'monitor'
5
+ require 'ipaddr'
5
6
 
6
7
  require_relative 'validate/version'
7
8
  require_relative 'validate/errors'
@@ -3,32 +3,12 @@ module Validate
3
3
  module ClassMethods
4
4
  def method_added(method_name)
5
5
  super
6
- return if @args.empty?
7
-
8
- method = instance_method(method_name)
9
- guard = ArgumentsGuard.new(method, @args.dup)
10
-
11
- @methods_guard.__send__(:define_method, method_name) do |*args, &block|
12
- guard.send(method_name, *args, &block)
13
- super(*args, &block)
14
- end
15
- ensure
16
- @args.clear
6
+ guard_method(instance_method(method_name), @methods_guard)
17
7
  end
18
8
 
19
9
  def singleton_method_added(method_name)
20
10
  super
21
- return if @args.empty?
22
-
23
- method = singleton_method(method_name)
24
- guard = ArgumentsGuard.new(method, @args.dup)
25
-
26
- @methods_guard.__send__(:define_singleton_method, method_name) do |*args, &block|
27
- guard.send(method_name, *args, &block)
28
- super(*args, &block)
29
- end
30
- ensure
31
- @args.clear
11
+ guard_method(singleton_method(method_name), @singleton_methods_guard)
32
12
  end
33
13
 
34
14
  def arg(name, &body)
@@ -39,6 +19,24 @@ module Validate
39
19
  @args[name] = Assertions.create(&body)
40
20
  self
41
21
  end
22
+
23
+ private
24
+
25
+ def guard_method(method, guard_module)
26
+ return if @args.empty?
27
+ guard = ArgumentsGuard.new(method, @args)
28
+ guard_module.__send__(:define_method, method.name) do |*args, **kwargs, &block|
29
+ if kwargs.empty?
30
+ guard.enforce!(*args, &block)
31
+ super(*args, &block)
32
+ else
33
+ guard.enforce!(*args, **kwargs, &block)
34
+ super(*args, **kwargs, &block)
35
+ end
36
+ end
37
+ ensure
38
+ @args = {}
39
+ end
42
40
  end
43
41
 
44
42
  def self.included(base)
@@ -46,6 +44,7 @@ module Validate
46
44
  base.instance_exec do
47
45
  @args = {}
48
46
  prepend(@methods_guard = Module.new)
47
+ singleton_class.prepend(@singleton_methods_guard = Module.new)
49
48
  end
50
49
  end
51
50
 
@@ -73,17 +72,19 @@ module Validate
73
72
  when :block
74
73
  signature << "&#{name}"
75
74
  else
76
- raise Error::ArgumentError, "unsupported parameter type #{kind}"
75
+ raise Error::ArgumentError,
76
+ "unsupported parameter type #{kind}"
77
77
  end
78
78
  next unless rules.include?(name)
79
79
 
80
- assertions << "@rules[:#{name}].assert(#{name}, message: 'invalid argument #{name}')"
80
+ assertions <<
81
+ "@rules[:#{name}].assert(#{name}, message: '#{name}') unless #{name}.eql?(DEFAULT_VALUE)"
81
82
  end
82
83
 
83
- singleton_class.class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
84
- def #{method.name}(#{signature.join(', ')})
85
- #{assertions.join("\n ")}
86
- end
84
+ singleton_class.class_eval(<<~RUBY, __FILE__, __LINE__)
85
+ def enforce!(#{signature.join(', ')})
86
+ #{assertions.join("\n ")}
87
+ end
87
88
  RUBY
88
89
 
89
90
  @rules = rules
@@ -5,8 +5,8 @@ module Validate
5
5
  module AST
6
6
  def self.build(*args, &block)
7
7
  Generator.new
8
- .generate(*args, &block)
9
- .freeze
8
+ .generate(*args, &block)
9
+ .freeze
10
10
  end
11
11
 
12
12
  class DefinitionContext
@@ -65,8 +65,8 @@ module Validate
65
65
 
66
66
  def evaluate(ctx)
67
67
  @constraints.each_value
68
- .reject { |c| catch(:pending) { c.valid?(ctx.value, ctx) } }
69
- .each { |c| ctx.add_violation(c) }
68
+ .reject { |c| catch(:pending) { c.valid?(ctx.value, ctx) } }
69
+ .each { |c| ctx.add_violation(c) }
70
70
  ctx
71
71
  end
72
72
  end
@@ -156,7 +156,7 @@ module Validate
156
156
  args.map { |arg| [:value, arg] },
157
157
  block,
158
158
  ::Kernel.caller
159
- .reject { |line| line.include?(__FILE__) }
159
+ .reject { |line| line.include?(__FILE__) }
160
160
  ]
161
161
  self
162
162
  end
@@ -286,10 +286,10 @@ module Validate
286
286
 
287
287
  def message
288
288
  'both ' + @constraints
289
- .size
290
- .times
291
- .map { |i| "[#{constraint_message(i)}]" }
292
- .join(', and ')
289
+ .size
290
+ .times
291
+ .map { |i| "[#{constraint_message(i)}]" }
292
+ .join(', and ')
293
293
  end
294
294
  end
295
295
 
@@ -327,10 +327,10 @@ module Validate
327
327
 
328
328
  def message
329
329
  'either ' + @constraints
330
- .size
331
- .times
332
- .map { |i| "[#{constraint_message(i)}]" }
333
- .join(', or ')
330
+ .size
331
+ .times
332
+ .map { |i| "[#{constraint_message(i)}]" }
333
+ .join(', or ')
334
334
  end
335
335
  end
336
336
 
@@ -364,10 +364,10 @@ module Validate
364
364
  return "not [#{constraint_message(0)}]" if @constraints.one?
365
365
 
366
366
  'neither ' + @constraints
367
- .size
368
- .times
369
- .map { |i| "[#{constraint_message(i)}]" }
370
- .join(', nor ')
367
+ .size
368
+ .times
369
+ .map { |i| "[#{constraint_message(i)}]" }
370
+ .join(', nor ')
371
371
  end
372
372
 
373
373
  def inspect
@@ -2,16 +2,34 @@
2
2
 
3
3
  module Validate
4
4
  module Compare
5
- class ByAttributes
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
6
19
  include Comparable
20
+ prepend TransformUsing
7
21
 
8
22
  def initialize(attributes)
9
23
  @attributes = attributes
10
24
  end
11
25
 
12
26
  def <=>(other)
13
- @attributes.map { |attribute, value| value <=> other.send(attribute) }
14
- .find { |result| !result.zero? } || 0
27
+ @attributes.each do |attribute, value|
28
+ result = value <=> other.send(attribute)
29
+ return result unless result.zero?
30
+ end
31
+
32
+ 0
15
33
  end
16
34
 
17
35
  def method_missing(symbol, *args)
@@ -23,12 +41,39 @@ module Validate
23
41
  def respond_to_missing?(attribute, _ = false)
24
42
  @attributes.include?(attribute)
25
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
26
66
  end
27
67
 
28
68
  module_function
29
69
 
30
70
  def attributes(**attributes)
31
- ByAttributes.new(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)
32
77
  end
33
78
  end
34
79
  end
@@ -116,13 +116,27 @@ module Validate
116
116
  expects_kwargs = true if kind == :keyrest
117
117
  end
118
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?
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
124
130
 
125
- merged_options = {}.merge!(super(*args, &block), known_options)
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
126
140
 
127
141
  options = supported_options.each_with_object({}) do |(n, opt), opts|
128
142
  opts[n] = opt.get_or_default(merged_options)
@@ -130,7 +144,7 @@ module Validate
130
144
 
131
145
  unless merged_options.empty?
132
146
  raise Error::ArgumentError,
133
- "undefined options #{merged_options.inspect}"
147
+ "unexpected options #{merged_options.inspect}"
134
148
  end
135
149
 
136
150
  @options = options.freeze
@@ -20,7 +20,7 @@ module Validate
20
20
  end
21
21
 
22
22
  @reserved_names[name] = name
23
- constraint_class = Constraint.create_class(name, defaults, &body)
23
+ constraint_class = Constraint.create_class(name, **defaults, &body)
24
24
  Constraints.const_set(Helpers.camelize(name), constraint_class)
25
25
  define_method(name, &constraint_class.method(:new))
26
26
  module_function(name)
@@ -104,6 +104,40 @@ module Validate
104
104
  key { "length_over_#{options[:min]}_under_#{options[:max]}" }
105
105
  end
106
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
+
107
141
  define(:one_of, message: 'be %{constraint.describe_presence}') do
108
142
  option(:values) do
109
143
  respond_to(:include?, message: 'values must respond to :include?')
@@ -309,6 +343,146 @@ module Validate
309
343
  end
310
344
  end
311
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
+
312
486
  define(
313
487
  :unique,
314
488
  message: 'have unique %{constraint.describe_unique_attribute}'
@@ -11,7 +11,12 @@ module Validate
11
11
  new(value, Path.new, violations)
12
12
  end
13
13
 
14
- attr_reader :value
14
+ def self.key(key, violations = [])
15
+ new(key, Path.new([KeyPath.new(key)]), violations)
16
+ end
17
+
18
+ attr_reader :value, :violations
19
+ protected :violations
15
20
 
16
21
  def initialize(value, path = Path.new, violations = [])
17
22
  @value = value
@@ -61,8 +66,17 @@ module Validate
61
66
  !@violations.empty?
62
67
  end
63
68
 
64
- def to_err
65
- Error::ConstraintViolationError.new(@violations.freeze)
69
+ def to_err(backtrace = [])
70
+ err = Error::ConstraintViolationError.new(@violations.freeze)
71
+ err.set_backtrace(backtrace)
72
+ err
73
+ end
74
+
75
+ def merge(other)
76
+ other.violations.each do |violation|
77
+ @violations << Constraint::Violation.new(violation.value, @path.child(violation.path), violation.constraint)
78
+ end
79
+ self
66
80
  end
67
81
 
68
82
  private
@@ -87,7 +101,14 @@ module Validate
87
101
  end
88
102
 
89
103
  def child(path)
90
- Path.new(@paths.dup << path)
104
+ case path
105
+ when KeyPath, AttrPath
106
+ Path.new(@paths.dup << path)
107
+ when Path
108
+ Path.new(@paths.dup << path.to_a)
109
+ else
110
+ raise ArgumentError, 'invalid path'
111
+ end
91
112
  end
92
113
 
93
114
  def to_s
@@ -31,9 +31,9 @@ module Validate
31
31
 
32
32
  def message
33
33
  @violations.group_by(&:path)
34
- .transform_values { |violations| violations.map(&:message) }
35
- .map { |path, messages| "#{path}: #{messages.join(' ')}" }
36
- .join("\n")
34
+ .transform_values { |violations| violations.map(&:message) }
35
+ .map { |path, messages| "#{path}: #{messages.join(', ')}" }
36
+ .join("\n")
37
37
  end
38
38
  end
39
39
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Validate
4
- VERSION = '0.1.0.alpha.1'
4
+ VERSION = '1.0.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: validate-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.alpha.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bulat Shakirzyanov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-07-20 00:00:00.000000000 Z
11
+ date: 2020-07-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aruba
@@ -150,9 +150,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
150
150
  version: 2.3.0
151
151
  required_rubygems_version: !ruby/object:Gem::Requirement
152
152
  requirements:
153
- - - ">"
153
+ - - ">="
154
154
  - !ruby/object:Gem::Version
155
- version: 1.3.1
155
+ version: '0'
156
156
  requirements: []
157
157
  rubygems_version: 3.0.3
158
158
  signing_key: