interactor-validation 0.3.2 → 0.3.4
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/CHANGELOG.md +31 -0
- data/README.md +1179 -44
- data/lib/interactor/validation/configuration.rb +6 -2
- data/lib/interactor/validation/validates.rb +91 -34
- data/lib/interactor/validation/version.rb +1 -1
- metadata +5 -6
|
@@ -4,16 +4,20 @@ module Interactor
|
|
|
4
4
|
module Validation
|
|
5
5
|
# Configuration class for interactor validation behavior
|
|
6
6
|
class Configuration
|
|
7
|
-
attr_accessor :
|
|
7
|
+
attr_accessor :halt, :regex_timeout, :max_array_size,
|
|
8
8
|
:enable_instrumentation, :cache_regex_patterns
|
|
9
9
|
attr_reader :error_mode
|
|
10
10
|
|
|
11
|
+
# Backward compatibility alias for halt_on_first_error
|
|
12
|
+
alias halt_on_first_error halt
|
|
13
|
+
alias halt_on_first_error= halt=
|
|
14
|
+
|
|
11
15
|
# Available error modes:
|
|
12
16
|
# - :default - Uses ActiveModel-style human-readable messages [DEFAULT]
|
|
13
17
|
# - :code - Returns structured error codes (e.g., USERNAME_IS_REQUIRED)
|
|
14
18
|
def initialize
|
|
15
19
|
@error_mode = :default
|
|
16
|
-
@
|
|
20
|
+
@halt = false
|
|
17
21
|
@regex_timeout = 0.1 # 100ms timeout for regex matching (ReDoS protection)
|
|
18
22
|
@max_array_size = 1000 # Maximum array size for nested validation (memory protection)
|
|
19
23
|
@enable_instrumentation = false # ActiveSupport::Notifications instrumentation
|
|
@@ -90,8 +90,33 @@ module Interactor
|
|
|
90
90
|
end
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
+
# Wrapper for ActiveModel::Errors that intercepts halt: option
|
|
94
|
+
class ErrorsWrapper < SimpleDelegator
|
|
95
|
+
def initialize(errors, interactor)
|
|
96
|
+
super(errors)
|
|
97
|
+
@interactor = interactor
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Override add to intercept halt: option
|
|
101
|
+
def add(attribute, message = :invalid, options = {})
|
|
102
|
+
# Extract halt option before passing to ActiveModel
|
|
103
|
+
halt = options.delete(:halt)
|
|
104
|
+
|
|
105
|
+
# Call the original add method
|
|
106
|
+
__getobj__.add(attribute, message, **options)
|
|
107
|
+
|
|
108
|
+
# Set halt flag if requested
|
|
109
|
+
@interactor.instance_variable_set(:@halt_validation, true) if halt
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
93
113
|
# Module prepended to override ActiveModel instance methods
|
|
94
114
|
module InstanceMethodsOverride
|
|
115
|
+
# Override errors to return a wrapper that intercepts halt: option
|
|
116
|
+
def errors
|
|
117
|
+
@errors ||= ErrorsWrapper.new(super, self)
|
|
118
|
+
end
|
|
119
|
+
|
|
95
120
|
# Override ActiveModel's validate! to prevent exception-raising behavior
|
|
96
121
|
# This hook is called automatically after validate_params!
|
|
97
122
|
# Users can override this to add custom validation logic
|
|
@@ -101,49 +126,55 @@ module Interactor
|
|
|
101
126
|
# super # Optional: call parent implementation if needed
|
|
102
127
|
# errors.add(:base, "Custom error") if some_condition?
|
|
103
128
|
# end
|
|
129
|
+
# rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
104
130
|
def validate!
|
|
105
131
|
# Preserve errors from validate_params! before calling super
|
|
106
132
|
# because super might trigger ActiveModel's valid? which clears errors
|
|
107
133
|
existing_error_details = errors.map do |error|
|
|
134
|
+
{ attribute: error.attribute, type: error.type, options: error.options }
|
|
135
|
+
rescue ArgumentError => e
|
|
136
|
+
# For anonymous classes, accessing error properties may fail
|
|
137
|
+
raise unless e.message.include?("Class name cannot be blank")
|
|
138
|
+
|
|
139
|
+
{ attribute: error.attribute, type: :invalid, options: {} }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Skip custom validations if halt was requested
|
|
143
|
+
unless @halt_validation
|
|
144
|
+
# Call super to allow class's validate! to run and add custom errors
|
|
145
|
+
# Rescue exceptions that might be raised
|
|
108
146
|
begin
|
|
109
|
-
|
|
147
|
+
super
|
|
148
|
+
rescue NoMethodError
|
|
149
|
+
# No parent validate! method, which is fine
|
|
110
150
|
rescue ArgumentError => e
|
|
111
|
-
# For anonymous classes,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
end
|
|
151
|
+
# For anonymous classes, super may fail when generating messages
|
|
152
|
+
raise unless e.message.include?("Class name cannot be blank")
|
|
153
|
+
rescue ActiveModel::ValidationError
|
|
154
|
+
# ActiveModel's validate! raises this when there are errors
|
|
155
|
+
# We handle errors differently, so just ignore this exception
|
|
117
156
|
end
|
|
118
157
|
end
|
|
119
158
|
|
|
120
|
-
# Call super to allow class's validate! to run and add custom errors
|
|
121
|
-
# Rescue exceptions that might be raised
|
|
122
|
-
begin
|
|
123
|
-
super
|
|
124
|
-
rescue NoMethodError
|
|
125
|
-
# No parent validate! method, which is fine
|
|
126
|
-
rescue ActiveModel::ValidationError
|
|
127
|
-
# ActiveModel's validate! raises this when there are errors
|
|
128
|
-
# We handle errors differently, so just ignore this exception
|
|
129
|
-
end
|
|
130
|
-
|
|
131
159
|
# Restore errors from validate_params! and add any new ones from custom validate!
|
|
132
160
|
existing_error_details.each do |error_detail|
|
|
133
161
|
# Only add if not already present (avoid duplicates)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
162
|
+
next if errors.where(error_detail[:attribute], error_detail[:type]).any?
|
|
163
|
+
|
|
164
|
+
begin
|
|
165
|
+
errors.add(error_detail[:attribute], error_detail[:type], **error_detail[:options])
|
|
166
|
+
rescue ArgumentError => e
|
|
167
|
+
# For anonymous classes, fall back to adding with message directly
|
|
168
|
+
raise unless e.message.include?("Class name cannot be blank")
|
|
169
|
+
|
|
170
|
+
message = error_detail[:options][:message] || error_detail[:type].to_s
|
|
171
|
+
errors.add(error_detail[:attribute], message)
|
|
144
172
|
end
|
|
145
173
|
end
|
|
146
174
|
|
|
175
|
+
# Reset halt flag after validation completes
|
|
176
|
+
@halt_validation = false
|
|
177
|
+
|
|
147
178
|
# Check all accumulated errors from validate_params! and this hook
|
|
148
179
|
# Fail the context if any errors exist
|
|
149
180
|
return if errors.empty?
|
|
@@ -151,6 +182,7 @@ module Interactor
|
|
|
151
182
|
# Use the existing formatted_errors method which handles all the edge cases
|
|
152
183
|
context.fail!(errors: formatted_errors)
|
|
153
184
|
end
|
|
185
|
+
# rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
154
186
|
end
|
|
155
187
|
|
|
156
188
|
private
|
|
@@ -162,6 +194,8 @@ module Interactor
|
|
|
162
194
|
def validate_params!
|
|
163
195
|
# Memoize config for performance
|
|
164
196
|
@current_config = current_config
|
|
197
|
+
# Initialize halt flag (but don't reset if already set)
|
|
198
|
+
@halt_validation ||= false
|
|
165
199
|
|
|
166
200
|
# Instrument validation if enabled
|
|
167
201
|
instrument("validate_params.interactor_validation") do
|
|
@@ -174,14 +208,17 @@ module Interactor
|
|
|
174
208
|
raise unless e.message.include?("Class name cannot be blank")
|
|
175
209
|
end
|
|
176
210
|
|
|
211
|
+
# Check if halt was requested during ActiveModel validations
|
|
212
|
+
return if @halt_validation || (@current_config.halt && errors.any?)
|
|
213
|
+
|
|
177
214
|
# Run our custom param validations after ActiveModel validations
|
|
178
215
|
self.class._param_validations.each do |param_name, rules|
|
|
179
216
|
# Safe param access - returns nil if not present in context
|
|
180
217
|
value = context.respond_to?(param_name) ? context.public_send(param_name) : nil
|
|
181
218
|
validate_param(param_name, value, rules)
|
|
182
219
|
|
|
183
|
-
# Halt on first error if configured
|
|
184
|
-
break if @current_config.
|
|
220
|
+
# Halt on first error if configured or if halt was explicitly requested
|
|
221
|
+
break if @halt_validation || (@current_config.halt && errors.any?)
|
|
185
222
|
end
|
|
186
223
|
|
|
187
224
|
# Don't fail here - let validate! hook handle failure
|
|
@@ -189,6 +226,7 @@ module Interactor
|
|
|
189
226
|
end
|
|
190
227
|
ensure
|
|
191
228
|
@current_config = nil # Clear memoization
|
|
229
|
+
# Don't reset @halt_validation here - let validate! handle it
|
|
192
230
|
end
|
|
193
231
|
|
|
194
232
|
# Get the current configuration (instance config overrides global config)
|
|
@@ -210,6 +248,7 @@ module Interactor
|
|
|
210
248
|
end
|
|
211
249
|
|
|
212
250
|
# Validates a single parameter with the given rules
|
|
251
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
213
252
|
def validate_param(param_name, value, rules)
|
|
214
253
|
# Skip validation if explicitly marked
|
|
215
254
|
return if rules[:_skip]
|
|
@@ -220,14 +259,25 @@ module Interactor
|
|
|
220
259
|
return
|
|
221
260
|
end
|
|
222
261
|
|
|
223
|
-
# Standard validations
|
|
262
|
+
# Standard validations - halt if requested or configured after each validation
|
|
224
263
|
validate_presence(param_name, value, rules)
|
|
264
|
+
return if @halt_validation || (@current_config.halt && errors.any?)
|
|
265
|
+
|
|
225
266
|
validate_boolean(param_name, value, rules)
|
|
267
|
+
return if @halt_validation || (@current_config.halt && errors.any?)
|
|
268
|
+
|
|
226
269
|
validate_format(param_name, value, rules)
|
|
270
|
+
return if @halt_validation || (@current_config.halt && errors.any?)
|
|
271
|
+
|
|
227
272
|
validate_length(param_name, value, rules)
|
|
273
|
+
return if @halt_validation || (@current_config.halt && errors.any?)
|
|
274
|
+
|
|
228
275
|
validate_inclusion(param_name, value, rules)
|
|
276
|
+
return if @halt_validation || (@current_config.halt && errors.any?)
|
|
277
|
+
|
|
229
278
|
validate_numericality(param_name, value, rules)
|
|
230
279
|
end
|
|
280
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
231
281
|
|
|
232
282
|
# Validates nested attributes in a hash or array
|
|
233
283
|
def validate_nested(param_name, value, nested_rules)
|
|
@@ -418,7 +468,7 @@ module Interactor
|
|
|
418
468
|
|
|
419
469
|
# Add error for nested validation
|
|
420
470
|
# rubocop:disable Metrics/ParameterLists
|
|
421
|
-
def add_nested_error(param_name, attr_name, custom_message, error_type, index: nil, **interpolations)
|
|
471
|
+
def add_nested_error(param_name, attr_name, custom_message, error_type, index: nil, halt: false, **interpolations)
|
|
422
472
|
# Build the attribute path for the error
|
|
423
473
|
attribute_path = if index.nil?
|
|
424
474
|
# Hash validation: param_name.attr_name
|
|
@@ -438,6 +488,9 @@ module Interactor
|
|
|
438
488
|
else
|
|
439
489
|
errors.add(attribute_path, error_type, **interpolations)
|
|
440
490
|
end
|
|
491
|
+
|
|
492
|
+
# Set halt flag if requested
|
|
493
|
+
@halt_validation = true if halt
|
|
441
494
|
end
|
|
442
495
|
# rubocop:enable Metrics/ParameterLists
|
|
443
496
|
|
|
@@ -630,8 +683,9 @@ module Interactor
|
|
|
630
683
|
# @param param_name [Symbol] the parameter name
|
|
631
684
|
# @param custom_message [String, nil] custom error message if provided
|
|
632
685
|
# @param error_type [Symbol] the type of validation error
|
|
686
|
+
# @param halt [Boolean] if true, halts validation immediately after adding this error
|
|
633
687
|
# @param interpolations [Hash] values to interpolate into the message
|
|
634
|
-
def add_error(param_name, custom_message, error_type, **interpolations)
|
|
688
|
+
def add_error(param_name, custom_message, error_type, halt: false, **interpolations)
|
|
635
689
|
if current_config.error_mode == :code
|
|
636
690
|
# Code mode: use custom message or generate code
|
|
637
691
|
code_message = custom_message || error_code_for(error_type, **interpolations)
|
|
@@ -642,6 +696,9 @@ module Interactor
|
|
|
642
696
|
else
|
|
643
697
|
errors.add(param_name, error_type, **interpolations)
|
|
644
698
|
end
|
|
699
|
+
|
|
700
|
+
# Set halt flag if requested
|
|
701
|
+
@halt_validation = true if halt
|
|
645
702
|
end
|
|
646
703
|
|
|
647
704
|
# Generate error code for :code mode using constants
|
|
@@ -700,7 +757,7 @@ module Interactor
|
|
|
700
757
|
param_name = format_attribute_for_code(error.attribute)
|
|
701
758
|
begin
|
|
702
759
|
message = error.message
|
|
703
|
-
rescue ArgumentError, NoMethodError
|
|
760
|
+
rescue ArgumentError, NoMethodError
|
|
704
761
|
# For anonymous classes or other edge cases, fall back to type
|
|
705
762
|
message = error.type.to_s.upcase
|
|
706
763
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: interactor-validation
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Wilson Anciro
|
|
@@ -13,28 +13,28 @@ dependencies:
|
|
|
13
13
|
name: activemodel
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
18
|
version: '6.0'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
|
-
- - "
|
|
23
|
+
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '6.0'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: activesupport
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
|
-
- - "
|
|
30
|
+
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
32
|
version: '6.0'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
|
-
- - "
|
|
37
|
+
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '6.0'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
@@ -133,7 +133,6 @@ licenses:
|
|
|
133
133
|
- MIT
|
|
134
134
|
metadata:
|
|
135
135
|
homepage_uri: https://github.com/zyxzen/interactor-validation
|
|
136
|
-
source_code_uri: https://github.com/zyxzen/interactor-validation
|
|
137
136
|
changelog_uri: https://github.com/zyxzen/interactor-validation/blob/main/CHANGELOG.md
|
|
138
137
|
rdoc_options: []
|
|
139
138
|
require_paths:
|