interactor-validation 0.3.9 → 0.4.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 +4 -4
- data/README.md +229 -1438
- data/benchmark/validation_benchmark.rb +0 -3
- data/lib/interactor/validation/configuration.rb +3 -27
- data/lib/interactor/validation/core_ext.rb +120 -0
- data/lib/interactor/validation/errors.rb +49 -0
- data/lib/interactor/validation/params.rb +6 -16
- data/lib/interactor/validation/validates.rb +118 -826
- data/lib/interactor/validation/validators/array.rb +20 -0
- data/lib/interactor/validation/validators/boolean.rb +15 -0
- data/lib/interactor/validation/validators/format.rb +17 -0
- data/lib/interactor/validation/validators/hash.rb +43 -0
- data/lib/interactor/validation/validators/inclusion.rb +17 -0
- data/lib/interactor/validation/validators/length.rb +46 -0
- data/lib/interactor/validation/validators/numeric.rb +46 -0
- data/lib/interactor/validation/validators/presence.rb +16 -0
- data/lib/interactor/validation/version.rb +1 -1
- data/lib/interactor/validation.rb +13 -44
- data/smoke_test.rb +252 -0
- metadata +28 -44
- data/lib/interactor/validation/error_codes.rb +0 -51
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
module Validation
|
|
5
|
+
module Validators
|
|
6
|
+
module Array
|
|
7
|
+
def validate_array(param, value, nested_rules)
|
|
8
|
+
return unless value.is_a?(::Array)
|
|
9
|
+
|
|
10
|
+
value.each_with_index do |item, idx|
|
|
11
|
+
validate_nested_item(param, item, nested_rules, idx)
|
|
12
|
+
|
|
13
|
+
# Halt on first error if configured
|
|
14
|
+
break if validation_config(:halt) && errors.any?
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
module Validation
|
|
5
|
+
module Validators
|
|
6
|
+
module Boolean
|
|
7
|
+
def validate_boolean(param, value)
|
|
8
|
+
return if [true, false].include?(value)
|
|
9
|
+
|
|
10
|
+
errors.add(param, :invalid, message: "must be true or false")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
module Validation
|
|
5
|
+
module Validators
|
|
6
|
+
module Format
|
|
7
|
+
def validate_format(param, value, rule)
|
|
8
|
+
pattern = rule.is_a?(::Hash) ? rule[:with] : rule
|
|
9
|
+
return if value.to_s.match?(pattern)
|
|
10
|
+
|
|
11
|
+
msg = rule.is_a?(::Hash) ? rule[:message] : nil
|
|
12
|
+
errors.add(param, :invalid, message: msg || "is invalid")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
module Validation
|
|
5
|
+
module Validators
|
|
6
|
+
module Hash
|
|
7
|
+
def validate_hash(param, value, nested_rules)
|
|
8
|
+
return unless value.is_a?(::Hash)
|
|
9
|
+
|
|
10
|
+
validate_nested_item(param, value, nested_rules)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def validate_nested_item(param, item, nested_rules, index = nil)
|
|
16
|
+
return unless item.is_a?(::Hash)
|
|
17
|
+
|
|
18
|
+
nested_rules.each do |attr, attr_rules|
|
|
19
|
+
attr_path = index.nil? ? "#{param}.#{attr}" : "#{param}[#{index}].#{attr}"
|
|
20
|
+
# Use key? to avoid false being treated as nil due to || operator
|
|
21
|
+
attr_value = item.key?(attr) ? item[attr] : item[attr.to_s]
|
|
22
|
+
validate_nested_attribute(attr_path.to_sym, attr_value, attr_rules)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def validate_nested_attribute(attr_path, value, rules)
|
|
27
|
+
if rules[:presence] && !value.present? && value != false
|
|
28
|
+
msg = rules[:presence].is_a?(::Hash) ? rules[:presence][:message] : nil
|
|
29
|
+
errors.add(attr_path, :blank, message: msg || "can't be blank")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
return unless value.present? || value == false
|
|
33
|
+
|
|
34
|
+
validate_format(attr_path, value, rules[:format]) if rules[:format]
|
|
35
|
+
validate_length(attr_path, value, rules[:length]) if rules[:length]
|
|
36
|
+
validate_inclusion(attr_path, value, rules[:inclusion]) if rules[:inclusion]
|
|
37
|
+
validate_numeric(attr_path, value, rules[:numeric]) if rules[:numeric]
|
|
38
|
+
validate_numeric(attr_path, value, rules[:numericality]) if rules[:numericality]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
module Validation
|
|
5
|
+
module Validators
|
|
6
|
+
module Inclusion
|
|
7
|
+
def validate_inclusion(param, value, rule)
|
|
8
|
+
allowed = rule.is_a?(::Hash) ? rule[:in] : rule
|
|
9
|
+
return if allowed.include?(value)
|
|
10
|
+
|
|
11
|
+
msg = rule.is_a?(::Hash) ? rule[:message] : nil
|
|
12
|
+
errors.add(param, :inclusion, message: msg || "is not included in the list")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
module Validation
|
|
5
|
+
module Validators
|
|
6
|
+
module Length
|
|
7
|
+
def validate_length(param, value, rule)
|
|
8
|
+
# Use .length for collections (Array/Hash), .to_s.length for other types
|
|
9
|
+
len = value.respond_to?(:length) && !value.is_a?(String) ? value.length : value.to_s.length
|
|
10
|
+
unit = collection?(value) ? "items" : "characters"
|
|
11
|
+
|
|
12
|
+
validate_minimum_length(param, len, rule[:minimum], unit, rule[:message]) if rule[:minimum]
|
|
13
|
+
validate_maximum_length(param, len, rule[:maximum], unit, rule[:message]) if rule[:maximum]
|
|
14
|
+
validate_exact_length(param, len, rule[:is], unit, rule[:message]) if rule[:is]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def collection?(value)
|
|
20
|
+
(value.is_a?(::Array) || value.is_a?(::Hash)) && !value.is_a?(String)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate_minimum_length(param, len, minimum, unit, custom_message)
|
|
24
|
+
return unless len < minimum
|
|
25
|
+
|
|
26
|
+
msg = custom_message || "is too short (minimum is #{minimum} #{unit})"
|
|
27
|
+
errors.add(param, :too_short, message: msg, count: minimum)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def validate_maximum_length(param, len, maximum, unit, custom_message)
|
|
31
|
+
return unless len > maximum
|
|
32
|
+
|
|
33
|
+
msg = custom_message || "is too long (maximum is #{maximum} #{unit})"
|
|
34
|
+
errors.add(param, :too_long, message: msg, count: maximum)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def validate_exact_length(param, len, exact, unit, custom_message)
|
|
38
|
+
return unless len != exact
|
|
39
|
+
|
|
40
|
+
msg = custom_message || "is the wrong length (should be #{exact} #{unit})"
|
|
41
|
+
errors.add(param, :wrong_length, message: msg, count: exact)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
module Validation
|
|
5
|
+
module Validators
|
|
6
|
+
module Numeric
|
|
7
|
+
def validate_numeric(param, value, rule)
|
|
8
|
+
unless numeric?(value)
|
|
9
|
+
msg = rule.is_a?(::Hash) ? rule[:message] : nil
|
|
10
|
+
errors.add(param, :not_a_number, message: msg || "is not a number")
|
|
11
|
+
return
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
num = coerce_numeric(value)
|
|
15
|
+
rule = {} unless rule.is_a?(::Hash)
|
|
16
|
+
|
|
17
|
+
check_numeric_constraint(param, num, :greater_than, rule) { |n, v| n <= v }
|
|
18
|
+
check_numeric_constraint(param, num, :greater_than_or_equal_to, rule) { |n, v| n < v }
|
|
19
|
+
check_numeric_constraint(param, num, :less_than, rule) { |n, v| n >= v }
|
|
20
|
+
check_numeric_constraint(param, num, :less_than_or_equal_to, rule) { |n, v| n > v }
|
|
21
|
+
check_numeric_constraint(param, num, :equal_to, rule) { |n, v| n != v }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def check_numeric_constraint(param, num, type, rule)
|
|
27
|
+
return unless rule[type]
|
|
28
|
+
return unless yield(num, rule[type])
|
|
29
|
+
|
|
30
|
+
msg = rule[:message] || "must be #{type.to_s.tr("_", " ")} #{rule[type]}"
|
|
31
|
+
errors.add(param, type, message: msg, count: rule[type])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def numeric?(value)
|
|
35
|
+
value.is_a?(::Numeric) || value.to_s.match?(/\A-?\d+(\.\d+)?\z/)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def coerce_numeric(value)
|
|
39
|
+
return value if value.is_a?(::Numeric)
|
|
40
|
+
|
|
41
|
+
value.to_s.include?(".") ? value.to_f : value.to_i
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
module Validation
|
|
5
|
+
module Validators
|
|
6
|
+
module Presence
|
|
7
|
+
def validate_presence(param, value, rule)
|
|
8
|
+
return if value.present? || value == false
|
|
9
|
+
|
|
10
|
+
msg = rule.is_a?(::Hash) ? rule[:message] : nil
|
|
11
|
+
errors.add(param, :blank, message: msg || "can't be blank")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -1,59 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "interactor"
|
|
4
|
-
|
|
5
|
-
require "active_model"
|
|
6
|
-
|
|
7
|
-
require_relative "validation/version"
|
|
4
|
+
require_relative "validation/core_ext"
|
|
8
5
|
require_relative "validation/configuration"
|
|
9
|
-
require_relative "validation/
|
|
6
|
+
require_relative "validation/errors"
|
|
10
7
|
require_relative "validation/params"
|
|
11
8
|
require_relative "validation/validates"
|
|
9
|
+
require_relative "validation/version"
|
|
12
10
|
|
|
13
11
|
module Interactor
|
|
14
12
|
module Validation
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
included do
|
|
20
|
-
include Params
|
|
21
|
-
include Validates
|
|
22
|
-
|
|
23
|
-
# Instance-level configuration (can override global config)
|
|
24
|
-
class_attribute :validation_config, instance_writer: false, default: nil
|
|
25
|
-
end
|
|
13
|
+
def self.included(base)
|
|
14
|
+
base.include Params
|
|
15
|
+
base.include Validates
|
|
16
|
+
base.before :run_validations! if base.respond_to?(:before)
|
|
26
17
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
# @example
|
|
30
|
-
# configure_validation do |config|
|
|
31
|
-
# config.error_mode = :code
|
|
32
|
-
# end
|
|
33
|
-
def configure_validation
|
|
34
|
-
self.validation_config ||= Configuration.new
|
|
35
|
-
yield(validation_config)
|
|
36
|
-
end
|
|
18
|
+
# Ensure before hook is set up on child classes
|
|
19
|
+
base.singleton_class.prepend(InheritanceHook) if base.respond_to?(:before)
|
|
37
20
|
end
|
|
38
21
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
base.class_eval do
|
|
44
|
-
# Register both validate_params! and validate! hooks
|
|
45
|
-
# Parameter validations run first, then custom validate! hook
|
|
46
|
-
before :validate_params! if respond_to?(:before)
|
|
47
|
-
before :validate! if respond_to?(:before)
|
|
48
|
-
|
|
49
|
-
# Set up inherited hook to ensure child classes also get the before hooks
|
|
50
|
-
def self.inherited(subclass)
|
|
51
|
-
super
|
|
52
|
-
subclass.before :validate_params! if subclass.respond_to?(:before)
|
|
53
|
-
subclass.before :validate! if subclass.respond_to?(:before)
|
|
54
|
-
# Also prepend InstanceMethodsOverride to child classes
|
|
55
|
-
subclass.prepend(Interactor::Validation::Validates::InstanceMethodsOverride)
|
|
56
|
-
end
|
|
22
|
+
module InheritanceHook
|
|
23
|
+
def inherited(subclass)
|
|
24
|
+
super
|
|
25
|
+
subclass.before :run_validations! if subclass.respond_to?(:before)
|
|
57
26
|
end
|
|
58
27
|
end
|
|
59
28
|
end
|
data/smoke_test.rb
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "lib/interactor/validation"
|
|
5
|
+
|
|
6
|
+
# Test 1: Basic validation
|
|
7
|
+
class TestBasicValidation
|
|
8
|
+
include Interactor
|
|
9
|
+
include Interactor::Validation
|
|
10
|
+
|
|
11
|
+
params :email, :username, :age
|
|
12
|
+
|
|
13
|
+
validates :email, presence: true, format: { with: /@/ }
|
|
14
|
+
validates :username, presence: true
|
|
15
|
+
validates :age, numericality: { greater_than: 0 }
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
context.result = "User created: #{email}, #{username}, #{age}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
puts "Test 1: Basic Validation"
|
|
23
|
+
puts "=" * 50
|
|
24
|
+
|
|
25
|
+
# Should succeed
|
|
26
|
+
result = TestBasicValidation.call(email: "user@example.com", username: "john", age: 25)
|
|
27
|
+
puts "✓ Valid params: #{result.success? ? 'PASS' : 'FAIL'}"
|
|
28
|
+
puts " Result: #{result.result}" if result.success?
|
|
29
|
+
|
|
30
|
+
# Should fail
|
|
31
|
+
result = TestBasicValidation.call(email: "", username: "", age: -5)
|
|
32
|
+
puts "✗ Invalid params: #{result.failure? ? 'PASS' : 'FAIL'}"
|
|
33
|
+
puts " Errors: #{result.errors.size} errors" if result.failure?
|
|
34
|
+
result.errors.each { |e| puts " - #{e[:attribute]}: #{e[:message]}" } if result.failure?
|
|
35
|
+
|
|
36
|
+
puts
|
|
37
|
+
|
|
38
|
+
# Test 2: Nested validation
|
|
39
|
+
class TestNestedValidation
|
|
40
|
+
include Interactor
|
|
41
|
+
include Interactor::Validation
|
|
42
|
+
|
|
43
|
+
params :user
|
|
44
|
+
|
|
45
|
+
validates :user, presence: true do
|
|
46
|
+
attribute :name, presence: true
|
|
47
|
+
attribute :email, format: { with: /@/ }
|
|
48
|
+
attribute :age, numericality: { greater_than: 0 }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def call
|
|
52
|
+
context.result = "User created: #{user[:name]}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
puts "Test 2: Nested Validation"
|
|
57
|
+
puts "=" * 50
|
|
58
|
+
|
|
59
|
+
# Should succeed
|
|
60
|
+
result = TestNestedValidation.call(user: { name: "John", email: "john@example.com", age: 30 })
|
|
61
|
+
puts "✓ Valid nested params: #{result.success? ? 'PASS' : 'FAIL'}"
|
|
62
|
+
puts " Result: #{result.result}" if result.success?
|
|
63
|
+
|
|
64
|
+
# Should fail
|
|
65
|
+
result = TestNestedValidation.call(user: { name: "", email: "invalid", age: -5 })
|
|
66
|
+
puts "✗ Invalid nested params: #{result.failure? ? 'PASS' : 'FAIL'}"
|
|
67
|
+
puts " Errors: #{result.errors.size} errors" if result.failure?
|
|
68
|
+
result.errors.each { |e| puts " - #{e[:attribute]}: #{e[:message]}" } if result.failure?
|
|
69
|
+
|
|
70
|
+
puts
|
|
71
|
+
|
|
72
|
+
# Test 3: Array validation
|
|
73
|
+
class TestArrayValidation
|
|
74
|
+
include Interactor
|
|
75
|
+
include Interactor::Validation
|
|
76
|
+
|
|
77
|
+
params :items
|
|
78
|
+
|
|
79
|
+
validates :items do
|
|
80
|
+
attribute :name, presence: true
|
|
81
|
+
attribute :price, numericality: { greater_than: 0 }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def call
|
|
85
|
+
context.result = "Processed #{items.size} items"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
puts "Test 3: Array Validation"
|
|
90
|
+
puts "=" * 50
|
|
91
|
+
|
|
92
|
+
# Should succeed
|
|
93
|
+
result = TestArrayValidation.call(items: [
|
|
94
|
+
{ name: "Widget", price: 10 },
|
|
95
|
+
{ name: "Gadget", price: 20 }
|
|
96
|
+
])
|
|
97
|
+
puts "✓ Valid array: #{result.success? ? 'PASS' : 'FAIL'}"
|
|
98
|
+
puts " Result: #{result.result}" if result.success?
|
|
99
|
+
|
|
100
|
+
# Should fail
|
|
101
|
+
result = TestArrayValidation.call(items: [
|
|
102
|
+
{ name: "Widget", price: 10 },
|
|
103
|
+
{ name: "", price: -5 }
|
|
104
|
+
])
|
|
105
|
+
puts "✗ Invalid array item: #{result.failure? ? 'PASS' : 'FAIL'}"
|
|
106
|
+
puts " Errors: #{result.errors.size} errors" if result.failure?
|
|
107
|
+
result.errors.each { |e| puts " - #{e[:attribute]}: #{e[:message]}" } if result.failure?
|
|
108
|
+
|
|
109
|
+
puts
|
|
110
|
+
|
|
111
|
+
# Test 4: Custom validation
|
|
112
|
+
class TestCustomValidation
|
|
113
|
+
include Interactor
|
|
114
|
+
include Interactor::Validation
|
|
115
|
+
|
|
116
|
+
params :product_id, :quantity
|
|
117
|
+
|
|
118
|
+
validates :product_id, presence: true
|
|
119
|
+
validates :quantity, numericality: { greater_than: 0 }
|
|
120
|
+
|
|
121
|
+
def validate!
|
|
122
|
+
super
|
|
123
|
+
|
|
124
|
+
# Custom business logic
|
|
125
|
+
if product_id == 999
|
|
126
|
+
errors.add(:product_id, :not_found, message: "Product not found")
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def call
|
|
131
|
+
context.result = "Order created for product #{product_id}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
puts "Test 4: Custom Validation"
|
|
136
|
+
puts "=" * 50
|
|
137
|
+
|
|
138
|
+
# Should succeed
|
|
139
|
+
result = TestCustomValidation.call(product_id: 123, quantity: 5)
|
|
140
|
+
puts "✓ Valid with custom validation: #{result.success? ? 'PASS' : 'FAIL'}"
|
|
141
|
+
puts " Result: #{result.result}" if result.success?
|
|
142
|
+
|
|
143
|
+
# Should fail with custom error
|
|
144
|
+
result = TestCustomValidation.call(product_id: 999, quantity: 5)
|
|
145
|
+
puts "✗ Custom validation error: #{result.failure? ? 'PASS' : 'FAIL'}"
|
|
146
|
+
puts " Errors: #{result.errors.size} errors" if result.failure?
|
|
147
|
+
result.errors.each { |e| puts " - #{e[:attribute]}: #{e[:message]}" } if result.failure?
|
|
148
|
+
|
|
149
|
+
puts
|
|
150
|
+
|
|
151
|
+
# Test 5: Halt on first error (disabled by default)
|
|
152
|
+
class TestHaltDisabled
|
|
153
|
+
include Interactor
|
|
154
|
+
include Interactor::Validation
|
|
155
|
+
|
|
156
|
+
params :email, :username, :age
|
|
157
|
+
|
|
158
|
+
validates :email, presence: true, format: { with: /@/ }
|
|
159
|
+
validates :username, presence: true
|
|
160
|
+
validates :age, numericality: { greater_than: 0 }
|
|
161
|
+
|
|
162
|
+
def call
|
|
163
|
+
context.result = "User created"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
puts "Test 5: Halt Disabled (Default Behavior)"
|
|
168
|
+
puts "=" * 50
|
|
169
|
+
|
|
170
|
+
# Should collect all errors
|
|
171
|
+
result = TestHaltDisabled.call(email: "", username: "", age: -5)
|
|
172
|
+
puts "✗ All errors collected: #{result.failure? && result.errors.size == 3 ? 'PASS' : 'FAIL'}"
|
|
173
|
+
puts " Expected 3 errors, got #{result.errors.size}" if result.failure?
|
|
174
|
+
result.errors.each { |e| puts " - #{e[:attribute]}: #{e[:type]}" } if result.failure?
|
|
175
|
+
|
|
176
|
+
puts
|
|
177
|
+
|
|
178
|
+
# Test 6: Halt on first error (enabled)
|
|
179
|
+
class TestHaltEnabled
|
|
180
|
+
include Interactor
|
|
181
|
+
include Interactor::Validation
|
|
182
|
+
|
|
183
|
+
params :email, :username, :age
|
|
184
|
+
|
|
185
|
+
validates :email, presence: true, format: { with: /@/ }
|
|
186
|
+
validates :username, presence: true
|
|
187
|
+
validates :age, numericality: { greater_than: 0 }
|
|
188
|
+
|
|
189
|
+
def call
|
|
190
|
+
context.result = "User created"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
puts "Test 6: Halt Enabled (Stop on First Error)"
|
|
195
|
+
puts "=" * 50
|
|
196
|
+
|
|
197
|
+
# Enable halt
|
|
198
|
+
Interactor::Validation.configure do |config|
|
|
199
|
+
config.halt = true
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Should stop at first error
|
|
203
|
+
result = TestHaltEnabled.call(email: "", username: "", age: -5)
|
|
204
|
+
puts "✗ Halted on first error: #{result.failure? && result.errors.size == 1 ? 'PASS' : 'FAIL'}"
|
|
205
|
+
puts " Expected 1 error, got #{result.errors.size}" if result.failure?
|
|
206
|
+
result.errors.each { |e| puts " - #{e[:attribute]}: #{e[:type]}" } if result.failure?
|
|
207
|
+
|
|
208
|
+
# Reset configuration
|
|
209
|
+
Interactor::Validation.configure do |config|
|
|
210
|
+
config.halt = false
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
puts
|
|
214
|
+
|
|
215
|
+
# Test 7: Halt with valid first param, invalid second
|
|
216
|
+
class TestHaltPartialErrors
|
|
217
|
+
include Interactor
|
|
218
|
+
include Interactor::Validation
|
|
219
|
+
|
|
220
|
+
params :email, :username, :age
|
|
221
|
+
|
|
222
|
+
validates :email, presence: true, format: { with: /@/ }
|
|
223
|
+
validates :username, presence: true, length: { minimum: 3 }
|
|
224
|
+
validates :age, numericality: { greater_than: 0 }
|
|
225
|
+
|
|
226
|
+
def call
|
|
227
|
+
context.result = "User created"
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
puts "Test 7: Halt with Valid First Param"
|
|
232
|
+
puts "=" * 50
|
|
233
|
+
|
|
234
|
+
# Enable halt
|
|
235
|
+
Interactor::Validation.configure do |config|
|
|
236
|
+
config.halt = true
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Valid email, invalid username and age - should stop at username
|
|
240
|
+
result = TestHaltPartialErrors.call(email: "valid@example.com", username: "", age: -5)
|
|
241
|
+
puts "✗ Halted at username error: #{result.failure? && result.errors.size == 1 && result.errors.first[:attribute] == :username ? 'PASS' : 'FAIL'}"
|
|
242
|
+
puts " Expected 1 error on username, got #{result.errors.size} errors" if result.failure?
|
|
243
|
+
result.errors.each { |e| puts " - #{e[:attribute]}: #{e[:type]}" } if result.failure?
|
|
244
|
+
|
|
245
|
+
# Reset configuration
|
|
246
|
+
Interactor::Validation.configure do |config|
|
|
247
|
+
config.halt = false
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
puts
|
|
251
|
+
puts "=" * 50
|
|
252
|
+
puts "Smoke tests complete!"
|