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.
@@ -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 :halt_on_first_error, :regex_timeout, :max_array_size,
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
- @halt_on_first_error = false
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
- { attribute: error.attribute, type: error.type, options: error.options }
147
+ super
148
+ rescue NoMethodError
149
+ # No parent validate! method, which is fine
110
150
  rescue ArgumentError => e
111
- # For anonymous classes, accessing error properties may fail
112
- if e.message.include?("Class name cannot be blank")
113
- { attribute: error.attribute, type: :invalid, options: {} }
114
- else
115
- raise
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
- unless errors.where(error_detail[:attribute], error_detail[:type]).any?
135
- begin
136
- errors.add(error_detail[:attribute], error_detail[:type], **error_detail[:options])
137
- rescue ArgumentError => e
138
- # For anonymous classes, fall back to adding with message directly
139
- raise unless e.message.include?("Class name cannot be blank")
140
-
141
- message = error_detail[:options][:message] || error_detail[:type].to_s
142
- errors.add(error_detail[:attribute], message)
143
- end
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.halt_on_first_error && errors.any?
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 => e
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Interactor
4
4
  module Validation
5
- VERSION = "0.3.2"
5
+ VERSION = "0.3.4"
6
6
  end
7
7
  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.2
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: