interactor-validation 0.2.0 → 0.3.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: bc137c0e8cdc24b828f1e2e1369f4527908b48d328df61e994e64ad736dbf59e
4
- data.tar.gz: b5c1bddd3dd946662b1394bbd5126aa8361798b59743f9f0da661bcf37166f37
3
+ metadata.gz: b8c04f34a47f6d887079d08a07958d91c19932a1b3c190faaa0ba4f1a85dbbf2
4
+ data.tar.gz: 167cbca80c87fe0519f3bab2a1e36c4d753366126e5587d3559907473d558812
5
5
  SHA512:
6
- metadata.gz: be6bb4b7fe4580208395dd82989f00de651b08bb90a5de35197307d2c9e4f2caff0b41b508a2b3ed973919fdf7bc03aa0c8a953cba9ea86dacc17ea5885cbb4e
7
- data.tar.gz: 14128318e687a03b13dc1bd0c3144f2db560ec2f2ed039289ee3c87d4726f5784720a72184d7693cbf55180b43782e405a7282f52e1b4a8f4311048991993bcf
6
+ metadata.gz: 03a955748e4429b6e141a17292484ec49bf52c5a8af056c7436270f91a9a536c1e782a6ca7d3af38b4437736dd960491735eb233c4abe3599d96a604d9297966
7
+ data.tar.gz: 0ba87d4e8591ff0ba7cb260d10bda55c8d0526bb8de75cc66a7c81b0addf2d5355d4f9da9919e3ad76fe3052e43868d7af39eb5d57729319ed92500ad12c5e61
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Interactor::Validation
2
2
 
3
- Add Rails-style parameter validation to your [Interactor](https://github.com/collectiveidea/interactor) service objects with simple, declarative syntax.
3
+ Add declarative parameter validation to your [Interactor](https://github.com/collectiveidea/interactor) service objects with Rails-style syntax.
4
4
 
5
5
  ## Installation
6
6
 
@@ -27,136 +27,165 @@ class CreateUser
27
27
  end
28
28
  end
29
29
 
30
- # Success
31
- result = CreateUser.call(email: "dev@example.com", username: "developer", age: 25)
32
- result.success? # => true
33
- result.user # => #<User...>
34
-
35
- # Failure - automatic validation
30
+ # Validation runs automatically before call
36
31
  result = CreateUser.call(email: "", username: "ab", age: -5)
37
32
  result.failure? # => true
38
33
  result.errors # => [
39
- # { code: "EMAIL_IS_REQUIRED" },
40
- # { code: "USERNAME_BELOW_MIN_LENGTH_3" },
41
- # { code: "AGE_MUST_BE_GREATER_THAN_0" }
34
+ # { attribute: :email, type: :blank, message: "Email can't be blank" },
35
+ # { attribute: :username, type: :too_short, message: "Username is too short (minimum is 3 characters)" },
36
+ # { attribute: :age, type: :greater_than, message: "Age must be greater than 0" }
42
37
  # ]
43
38
  ```
44
39
 
45
- ## Features
46
-
47
- ### Parameter Declaration
48
-
49
- Use `params` to declare expected parameters - they're automatically delegated to context:
50
-
51
- ```ruby
52
- params :user_id, :action
53
-
54
- def call
55
- # Access directly instead of context.user_id
56
- user = User.find(user_id)
57
- user.perform(action)
58
- end
59
- ```
60
-
61
- ### Validation Rules
40
+ ## Validation Types
62
41
 
63
- All validations run **before** your `call` method. If validation fails, the interactor stops and returns structured errors.
42
+ ### Presence
64
43
 
65
- **Presence**
66
44
  ```ruby
67
45
  validates :name, presence: true
68
- # Error: { code: "NAME_IS_REQUIRED" }
46
+ # Error: { attribute: :name, type: :blank, message: "Name can't be blank" }
69
47
  ```
70
48
 
71
- **Format (Regex)**
49
+ ### Format (Regex)
50
+
72
51
  ```ruby
73
52
  validates :email, format: { with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i }
74
- # Error: { code: "EMAIL_INVALID_FORMAT" }
53
+ # Error: { attribute: :email, type: :invalid, message: "Email is invalid" }
75
54
  ```
76
55
 
77
- **Length**
56
+ ### Length
57
+
78
58
  ```ruby
79
59
  validates :password, length: { minimum: 8, maximum: 128 }
80
60
  validates :code, length: { is: 6 }
81
- # Errors: { code: "PASSWORD_BELOW_MIN_LENGTH_8" }
82
- # { code: "CODE_INVALID_LENGTH_6" }
61
+ # Errors: { attribute: :password, type: :too_short, message: "Password is too short (minimum is 8 characters)" }
62
+ # { attribute: :code, type: :wrong_length, message: "Code is the wrong length (should be 6 characters)" }
83
63
  ```
84
64
 
85
- **Inclusion**
65
+ ### Inclusion
66
+
86
67
  ```ruby
87
68
  validates :status, inclusion: { in: %w[active pending inactive] }
88
- # Error: { code: "STATUS_NOT_IN_LIST" }
69
+ # Error: { attribute: :status, type: :inclusion, message: "Status is not included in the list" }
89
70
  ```
90
71
 
91
- **Numericality**
72
+ ### Numericality
73
+
92
74
  ```ruby
93
75
  validates :price, numericality: { greater_than_or_equal_to: 0 }
94
76
  validates :quantity, numericality: { greater_than: 0, less_than_or_equal_to: 100 }
95
- # Errors: { code: "PRICE_MUST_BE_GREATER_THAN_OR_EQUAL_TO_0" }
77
+ validates :count, numericality: true # Just check if numeric
78
+
79
+ # Available constraints:
80
+ # - greater_than, greater_than_or_equal_to
81
+ # - less_than, less_than_or_equal_to
82
+ # - equal_to
83
+ ```
84
+
85
+ ### Boolean
86
+
87
+ ```ruby
88
+ validates :is_active, boolean: true
89
+ # Ensures value is true or false (not truthy/falsy)
90
+ ```
91
+
92
+ ### Nested Validation
93
+
94
+ Validate nested hashes and arrays:
95
+
96
+ ```ruby
97
+ # Hash validation
98
+ params :user
99
+ validates :user do
100
+ attribute :name, presence: true
101
+ attribute :email, format: { with: /@/ }
102
+ attribute :age, numericality: { greater_than: 0 }
103
+ end
104
+
105
+ # Array validation
106
+ params :items
107
+ validates :items do
108
+ attribute :name, presence: true
109
+ attribute :price, numericality: { greater_than: 0 }
110
+ end
96
111
  ```
97
112
 
98
- ### Error Formats
113
+ ## Error Formats
99
114
 
100
- The gem supports two error formatting modes:
115
+ Choose between two error format modes:
101
116
 
102
- #### Code Mode (Default)
117
+ ### Default Mode (ActiveModel-style)
103
118
 
104
- Returns structured error codes ideal for APIs and i18n:
119
+ Human-readable errors with full context - ideal for forms and user-facing applications:
105
120
 
106
121
  ```ruby
122
+ # This is the default, no configuration needed
107
123
  result.errors # => [
108
- # { code: "EMAIL_IS_REQUIRED" },
109
- # { code: "USERNAME_BELOW_MIN_LENGTH_3" }
124
+ # { attribute: :email, type: :blank, message: "Email can't be blank" },
125
+ # { attribute: :username, type: :too_short, message: "Username is too short" }
110
126
  # ]
111
127
  ```
112
128
 
113
- #### Default Mode
129
+ ### Code Mode
114
130
 
115
- Returns ActiveModel-style errors with human-readable messages:
131
+ Structured error codes - ideal for APIs and internationalization:
116
132
 
117
133
  ```ruby
118
- Interactor::Validation.configure do |config|
119
- config.error_mode = :default
134
+ configure_validation do |config|
135
+ config.error_mode = :code
120
136
  end
121
137
 
122
138
  result.errors # => [
123
- # { attribute: :email, type: :blank, message: "Email can't be blank" },
124
- # { attribute: :username, type: :too_short, message: "Username is too short (minimum is 3 characters)" }
139
+ # { code: "EMAIL_IS_REQUIRED" },
140
+ # { code: "USERNAME_BELOW_MIN_LENGTH_3" }
125
141
  # ]
126
142
  ```
127
143
 
144
+ ### Custom Messages
145
+
146
+ Provide custom error messages for any validation:
147
+
148
+ ```ruby
149
+ # Works with both modes
150
+ validates :username, presence: { message: "Username is required" }
151
+ validates :email, format: { with: /@/, message: "Invalid email format" }
152
+
153
+ # In :code mode, custom messages become part of the code
154
+ configure_validation { |c| c.error_mode = :code }
155
+ validates :age, numericality: { greater_than: 0, message: "INVALID_AGE" }
156
+ # => { code: "AGE_INVALID_AGE" }
157
+ ```
158
+
128
159
  ## Configuration
129
160
 
130
161
  ### Global Configuration
131
162
 
132
- Configure validation behavior for all interactors:
163
+ Configure behavior for all interactors:
133
164
 
134
165
  ```ruby
135
166
  # config/initializers/interactor_validation.rb
136
167
  Interactor::Validation.configure do |config|
137
- # Error format mode: :code (default) or :default
138
- config.error_mode = :code
168
+ # Error format: :default (ActiveModel-style) or :code (structured codes)
169
+ config.error_mode = :default
139
170
 
140
- # Stop validation at first error (default: false)
171
+ # Stop at first error for better performance
141
172
  config.halt_on_first_error = false
142
173
 
143
- # Security: Regex timeout in seconds (default: 0.1)
144
- config.regex_timeout = 0.1
145
-
146
- # Security: Maximum array size for nested validation (default: 1000)
147
- config.max_array_size = 1000
174
+ # Security settings
175
+ config.regex_timeout = 0.1 # Regex timeout in seconds (ReDoS protection)
176
+ config.max_array_size = 1000 # Max array size for nested validation
148
177
 
149
- # Performance: Cache compiled regex patterns (default: true)
178
+ # Performance settings
150
179
  config.cache_regex_patterns = true
151
180
 
152
- # Monitoring: Enable ActiveSupport::Notifications (default: false)
181
+ # Monitoring
153
182
  config.enable_instrumentation = false
154
183
  end
155
184
  ```
156
185
 
157
186
  ### Per-Interactor Configuration
158
187
 
159
- Override global settings for specific interactors:
188
+ Override settings for specific interactors:
160
189
 
161
190
  ```ruby
162
191
  class CreateUser
@@ -164,7 +193,7 @@ class CreateUser
164
193
  include Interactor::Validation
165
194
 
166
195
  configure_validation do |config|
167
- config.error_mode = :default
196
+ config.error_mode = :code
168
197
  config.halt_on_first_error = true
169
198
  end
170
199
 
@@ -173,47 +202,37 @@ class CreateUser
173
202
  end
174
203
  ```
175
204
 
176
- ### Custom Error Messages
205
+ ## Advanced Features
177
206
 
178
- Provide custom error messages for any validation:
207
+ ### Parameter Declaration
179
208
 
180
- ```ruby
181
- # With :code mode
182
- configure_validation do |config|
183
- config.error_mode = :code
184
- end
209
+ Declare parameters for automatic delegation to context:
185
210
 
186
- validates :username, presence: { message: "CUSTOM_REQUIRED_ERROR" }
187
- validates :email, format: { with: /@/, message: "CUSTOM_FORMAT_ERROR" }
188
- # => { code: "USERNAME_CUSTOM_REQUIRED_ERROR" }
189
- # => { code: "EMAIL_CUSTOM_FORMAT_ERROR" }
211
+ ```ruby
212
+ params :user_id, :action
190
213
 
191
- # With :default mode
192
- configure_validation do |config|
193
- config.error_mode = :default
214
+ def call
215
+ # Access directly instead of context.user_id
216
+ user = User.find(user_id)
217
+ user.perform(action)
194
218
  end
195
-
196
- validates :bio, length: { maximum: 500, message: "is too long (max 500 chars)" }
197
- # => { attribute: :bio, type: :too_long, message: "is too long (max 500 chars)" }
198
219
  ```
199
220
 
200
- ### Advanced Usage
201
-
202
- #### Halt on First Error
221
+ ### Halt on First Error
203
222
 
204
- Improve performance by stopping validation at the first failure:
223
+ Improve performance by stopping validation early:
205
224
 
206
225
  ```ruby
207
226
  configure_validation do |config|
208
- config.halt_on_first_error = true # Stop at first error
227
+ config.halt_on_first_error = true
209
228
  end
210
229
 
211
230
  validates :field1, presence: true
212
231
  validates :field2, presence: true # Won't run if field1 fails
213
- validates :field3, presence: true # Won't run if field1 or field2 fails
232
+ validates :field3, presence: true # Won't run if earlier fields fail
214
233
  ```
215
234
 
216
- #### Integration with ActiveModel Validations
235
+ ### ActiveModel Integration
217
236
 
218
237
  Use ActiveModel's custom validation callbacks:
219
238
 
@@ -224,88 +243,90 @@ class CreateUser
224
243
 
225
244
  params :user_data
226
245
 
227
- validate :check_user_data_structure
246
+ validate :check_custom_logic
228
247
  validates :username, presence: true
229
248
 
230
- def check_user_data_structure
231
- errors.add(:user_data, "must be a Hash") unless user_data.is_a?(Hash)
249
+ private
250
+
251
+ def check_custom_logic
252
+ errors.add(:base, "Custom validation failed") unless custom_condition?
232
253
  end
233
254
  end
234
255
  ```
235
256
 
236
- ## Security
257
+ ### Performance Monitoring
237
258
 
238
- This gem includes built-in protection against common security vulnerabilities:
239
-
240
- ### ReDoS Protection (v0.2.0+)
241
-
242
- Regular Expression Denial of Service attacks are prevented with automatic timeouts:
259
+ Track validation performance in production:
243
260
 
244
261
  ```ruby
245
- config.regex_timeout = 0.1 # 100ms default timeout
246
- ```
247
-
248
- If a regex takes longer than the configured timeout, validation will fail safely instead of hanging.
249
-
250
- ### Memory Protection (v0.2.0+)
251
-
252
- Array validation includes automatic size limits to prevent memory exhaustion:
262
+ config.enable_instrumentation = true
253
263
 
254
- ```ruby
255
- config.max_array_size = 1000 # Default limit
264
+ ActiveSupport::Notifications.subscribe('validate_params.interactor_validation') do |*args|
265
+ event = ActiveSupport::Notifications::Event.new(*args)
266
+ Rails.logger.info "Validation: #{event.duration}ms (#{event.payload[:interactor]})"
267
+ end
256
268
  ```
257
269
 
258
- ### Thread Safety (v0.2.0+)
270
+ ## Security
259
271
 
260
- Validation rule registration is thread-safe and can be used safely in multi-threaded environments (Puma, Sidekiq).
272
+ Built-in protection against common vulnerabilities:
261
273
 
262
- ### Best Practices
274
+ ### ReDoS Protection
263
275
 
264
- 1. **Use simple regex patterns** - Avoid nested quantifiers that can cause backtracking
265
- 2. **Sanitize outputs** - Always escape error messages when rendering in HTML
266
- 3. **Set appropriate limits** - Configure `max_array_size` based on your application needs
267
- 4. **Monitor performance** - Enable instrumentation in production to detect slow validations
276
+ Automatic timeouts prevent Regular Expression Denial of Service attacks:
268
277
 
269
- For detailed security information, see [SECURITY.md](SECURITY.md).
278
+ ```ruby
279
+ config.regex_timeout = 0.1 # 100ms default
280
+ ```
270
281
 
271
- ## Performance
282
+ If a regex exceeds the timeout, validation fails safely instead of hanging.
272
283
 
273
- ### Benchmarking
284
+ ### Memory Protection
274
285
 
275
- Run the included benchmark suite to measure performance:
286
+ Array size limits prevent memory exhaustion:
276
287
 
277
- ```bash
278
- bundle exec ruby benchmark/validation_benchmark.rb
288
+ ```ruby
289
+ config.max_array_size = 1000 # Default limit
279
290
  ```
280
291
 
281
- ### Monitoring
292
+ ### Thread Safety
282
293
 
283
- Enable instrumentation to track validation performance in production:
294
+ All validation operations are thread-safe for use with Puma, Sidekiq, etc.
284
295
 
285
- ```ruby
286
- config.enable_instrumentation = true
296
+ ### Best Practices
287
297
 
288
- ActiveSupport::Notifications.subscribe('validate_params.interactor_validation') do |*args|
289
- event = ActiveSupport::Notifications::Event.new(*args)
290
- Rails.logger.info "Validation took #{event.duration}ms for #{event.payload[:interactor]}"
291
- end
292
- ```
298
+ - Use simple regex patterns (avoid nested quantifiers)
299
+ - Sanitize error messages before displaying in HTML
300
+ - Set appropriate `max_array_size` limits for your use case
301
+ - Enable instrumentation to monitor performance
302
+ - Review [SECURITY.md](SECURITY.md) for detailed information
293
303
 
294
304
  ## Development
295
305
 
296
306
  ```bash
297
307
  bin/setup # Install dependencies
298
- bundle exec rspec # Run tests
308
+ bundle exec rspec # Run tests (231 examples)
299
309
  bundle exec rubocop # Lint code
300
310
  bin/console # Interactive console
301
311
  ```
302
312
 
313
+ ### Benchmarking
314
+
315
+ ```bash
316
+ bundle exec ruby benchmark/validation_benchmark.rb
317
+ ```
318
+
303
319
  ## Requirements
304
320
 
305
321
  - Ruby >= 3.2.0
306
322
  - Interactor ~> 3.0
307
323
  - ActiveModel >= 6.0
324
+ - ActiveSupport >= 6.0
308
325
 
309
326
  ## License
310
327
 
311
328
  MIT License - see [LICENSE.txt](LICENSE.txt)
329
+
330
+ ## Contributing
331
+
332
+ Issues and pull requests are welcome at [https://github.com/zyxzen/interactor-validation](https://github.com/zyxzen/interactor-validation)
@@ -9,10 +9,10 @@ module Interactor
9
9
  attr_reader :error_mode
10
10
 
11
11
  # Available error modes:
12
- # - :default - Uses ActiveModel-style human-readable messages
13
- # - :code - Returns structured error codes (e.g., USERNAME_IS_REQUIRED) [DEFAULT]
12
+ # - :default - Uses ActiveModel-style human-readable messages [DEFAULT]
13
+ # - :code - Returns structured error codes (e.g., USERNAME_IS_REQUIRED)
14
14
  def initialize
15
- @error_mode = :code
15
+ @error_mode = :default
16
16
  @halt_on_first_error = false
17
17
  @regex_timeout = 0.1 # 100ms timeout for regex matching (ReDoS protection)
18
18
  @max_array_size = 1000 # Maximum array size for nested validation (memory protection)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Interactor
4
4
  module Validation
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wilson Anciro