interactor-validation 0.1.1 → 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 +4 -4
- data/CHANGELOG.md +57 -0
- data/README.md +176 -85
- data/benchmark/validation_benchmark.rb +227 -0
- data/lib/interactor/validation/configuration.rb +9 -4
- data/lib/interactor/validation/error_codes.rb +51 -0
- data/lib/interactor/validation/validates.rb +460 -44
- data/lib/interactor/validation/version.rb +1 -1
- data/lib/interactor/validation.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8c04f34a47f6d887079d08a07958d91c19932a1b3c190faaa0ba4f1a85dbbf2
|
|
4
|
+
data.tar.gz: 167cbca80c87fe0519f3bab2a1e36c4d753366126e5587d3559907473d558812
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 03a955748e4429b6e141a17292484ec49bf52c5a8af056c7436270f91a9a536c1e782a6ca7d3af38b4437736dd960491735eb233c4abe3599d96a604d9297966
|
|
7
|
+
data.tar.gz: 0ba87d4e8591ff0ba7cb260d10bda55c8d0526bb8de75cc66a7c81b0addf2d5355d4f9da9919e3ad76fe3052e43868d7af39eb5d57729319ed92500ad12c5e61
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2024-11-16
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
|
|
7
|
+
- **CRITICAL:** Added ReDoS (Regular Expression Denial of Service) protection with configurable timeout
|
|
8
|
+
- **HIGH:** Fixed thread safety issues in validation rule registration using mutex locks
|
|
9
|
+
- Added memory protection with configurable maximum array size for nested validations
|
|
10
|
+
- Added regex pattern caching to prevent recompilation attacks
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
- Fixed numeric precision loss bug - now uses `to_i` for integers and `to_f` for floats
|
|
15
|
+
- Fixed ambiguous handling of `nil` vs missing hash keys in nested validation
|
|
16
|
+
- Improved boolean validation to properly distinguish between `nil`, `false`, and missing values
|
|
17
|
+
|
|
18
|
+
### Performance Improvements
|
|
19
|
+
|
|
20
|
+
- Implemented regex pattern caching for up to 10x performance improvement on repeated validations
|
|
21
|
+
- Added configuration memoization during validation to reduce overhead
|
|
22
|
+
- Optimized string-to-numeric coercion to preserve integer precision
|
|
23
|
+
|
|
24
|
+
### New Features
|
|
25
|
+
|
|
26
|
+
- Added `config.regex_timeout` - Configurable timeout for regex validation (default: 100ms)
|
|
27
|
+
- Added `config.max_array_size` - Maximum array size for nested validation (default: 1000)
|
|
28
|
+
- Added `config.enable_instrumentation` - ActiveSupport::Notifications integration for monitoring
|
|
29
|
+
- Added `config.cache_regex_patterns` - Enable/disable regex pattern caching (default: true)
|
|
30
|
+
- Created `ErrorCodes` module with constants for all error types
|
|
31
|
+
- Added comprehensive YARD documentation for public API methods
|
|
32
|
+
|
|
33
|
+
### Documentation
|
|
34
|
+
|
|
35
|
+
- Added SECURITY.md with vulnerability reporting process and security best practices
|
|
36
|
+
- Added benchmark suite in `benchmark/validation_benchmark.rb`
|
|
37
|
+
- Enhanced inline documentation with YARD tags for all public methods
|
|
38
|
+
- Improved code organization by extracting error codes into separate module
|
|
39
|
+
|
|
40
|
+
### Breaking Changes
|
|
41
|
+
|
|
42
|
+
None - this release is fully backward compatible with 0.1.x
|
|
43
|
+
|
|
44
|
+
### Upgrade Notes
|
|
45
|
+
|
|
46
|
+
To take advantage of the new security features, no changes are required. However, you may want to configure:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
Interactor::Validation.configure do |config|
|
|
50
|
+
config.regex_timeout = 0.05 # Stricter timeout for high-security contexts
|
|
51
|
+
config.max_array_size = 100 # Lower limit for your use case
|
|
52
|
+
config.enable_instrumentation = true # Monitor validation performance
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## [0.1.1] - 2025-11-16
|
|
57
|
+
|
|
58
|
+
- Minor version bump for release preparation
|
|
59
|
+
|
|
3
60
|
## [0.1.0] - 2025-11-16
|
|
4
61
|
|
|
5
62
|
- Initial release
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Interactor::Validation
|
|
2
2
|
|
|
3
|
-
Add
|
|
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,124 +27,165 @@ class CreateUser
|
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
#
|
|
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
|
-
# {
|
|
40
|
-
# {
|
|
41
|
-
# {
|
|
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
|
-
##
|
|
46
|
-
|
|
47
|
-
### Parameter Declaration
|
|
40
|
+
## Validation Types
|
|
48
41
|
|
|
49
|
-
|
|
42
|
+
### Presence
|
|
50
43
|
|
|
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
|
|
62
|
-
|
|
63
|
-
All validations run **before** your `call` method. If validation fails, the interactor stops and returns structured errors.
|
|
64
|
-
|
|
65
|
-
**Presence**
|
|
66
44
|
```ruby
|
|
67
45
|
validates :name, presence: true
|
|
68
|
-
# Error: {
|
|
46
|
+
# Error: { attribute: :name, type: :blank, message: "Name can't be blank" }
|
|
69
47
|
```
|
|
70
48
|
|
|
71
|
-
|
|
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: {
|
|
53
|
+
# Error: { attribute: :email, type: :invalid, message: "Email is invalid" }
|
|
75
54
|
```
|
|
76
55
|
|
|
77
|
-
|
|
56
|
+
### Length
|
|
57
|
+
|
|
78
58
|
```ruby
|
|
79
59
|
validates :password, length: { minimum: 8, maximum: 128 }
|
|
80
60
|
validates :code, length: { is: 6 }
|
|
81
|
-
# Errors: {
|
|
82
|
-
# { code: "
|
|
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
|
-
|
|
65
|
+
### Inclusion
|
|
66
|
+
|
|
86
67
|
```ruby
|
|
87
68
|
validates :status, inclusion: { in: %w[active pending inactive] }
|
|
88
|
-
# Error: {
|
|
69
|
+
# Error: { attribute: :status, type: :inclusion, message: "Status is not included in the list" }
|
|
89
70
|
```
|
|
90
71
|
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
+
## Error Formats
|
|
99
114
|
|
|
100
|
-
|
|
115
|
+
Choose between two error format modes:
|
|
101
116
|
|
|
102
|
-
|
|
117
|
+
### Default Mode (ActiveModel-style)
|
|
103
118
|
|
|
104
|
-
|
|
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
|
-
# {
|
|
109
|
-
# {
|
|
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
|
-
|
|
129
|
+
### Code Mode
|
|
114
130
|
|
|
115
|
-
|
|
131
|
+
Structured error codes - ideal for APIs and internationalization:
|
|
116
132
|
|
|
117
133
|
```ruby
|
|
118
|
-
|
|
119
|
-
config.error_mode = :
|
|
134
|
+
configure_validation do |config|
|
|
135
|
+
config.error_mode = :code
|
|
120
136
|
end
|
|
121
137
|
|
|
122
138
|
result.errors # => [
|
|
123
|
-
# {
|
|
124
|
-
# {
|
|
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
|
|
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
|
|
138
|
-
config.error_mode = :
|
|
168
|
+
# Error format: :default (ActiveModel-style) or :code (structured codes)
|
|
169
|
+
config.error_mode = :default
|
|
139
170
|
|
|
140
|
-
# Stop
|
|
171
|
+
# Stop at first error for better performance
|
|
141
172
|
config.halt_on_first_error = false
|
|
173
|
+
|
|
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
|
|
177
|
+
|
|
178
|
+
# Performance settings
|
|
179
|
+
config.cache_regex_patterns = true
|
|
180
|
+
|
|
181
|
+
# Monitoring
|
|
182
|
+
config.enable_instrumentation = false
|
|
142
183
|
end
|
|
143
184
|
```
|
|
144
185
|
|
|
145
186
|
### Per-Interactor Configuration
|
|
146
187
|
|
|
147
|
-
Override
|
|
188
|
+
Override settings for specific interactors:
|
|
148
189
|
|
|
149
190
|
```ruby
|
|
150
191
|
class CreateUser
|
|
@@ -152,7 +193,7 @@ class CreateUser
|
|
|
152
193
|
include Interactor::Validation
|
|
153
194
|
|
|
154
195
|
configure_validation do |config|
|
|
155
|
-
config.error_mode = :
|
|
196
|
+
config.error_mode = :code
|
|
156
197
|
config.halt_on_first_error = true
|
|
157
198
|
end
|
|
158
199
|
|
|
@@ -161,47 +202,37 @@ class CreateUser
|
|
|
161
202
|
end
|
|
162
203
|
```
|
|
163
204
|
|
|
164
|
-
|
|
205
|
+
## Advanced Features
|
|
165
206
|
|
|
166
|
-
|
|
207
|
+
### Parameter Declaration
|
|
167
208
|
|
|
168
|
-
|
|
169
|
-
# With :code mode
|
|
170
|
-
configure_validation do |config|
|
|
171
|
-
config.error_mode = :code
|
|
172
|
-
end
|
|
209
|
+
Declare parameters for automatic delegation to context:
|
|
173
210
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
# => { code: "USERNAME_CUSTOM_REQUIRED_ERROR" }
|
|
177
|
-
# => { code: "EMAIL_CUSTOM_FORMAT_ERROR" }
|
|
211
|
+
```ruby
|
|
212
|
+
params :user_id, :action
|
|
178
213
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
214
|
+
def call
|
|
215
|
+
# Access directly instead of context.user_id
|
|
216
|
+
user = User.find(user_id)
|
|
217
|
+
user.perform(action)
|
|
182
218
|
end
|
|
183
|
-
|
|
184
|
-
validates :bio, length: { maximum: 500, message: "is too long (max 500 chars)" }
|
|
185
|
-
# => { attribute: :bio, type: :too_long, message: "is too long (max 500 chars)" }
|
|
186
219
|
```
|
|
187
220
|
|
|
188
|
-
###
|
|
189
|
-
|
|
190
|
-
#### Halt on First Error
|
|
221
|
+
### Halt on First Error
|
|
191
222
|
|
|
192
|
-
Improve performance by stopping validation
|
|
223
|
+
Improve performance by stopping validation early:
|
|
193
224
|
|
|
194
225
|
```ruby
|
|
195
226
|
configure_validation do |config|
|
|
196
|
-
config.halt_on_first_error = true
|
|
227
|
+
config.halt_on_first_error = true
|
|
197
228
|
end
|
|
198
229
|
|
|
199
230
|
validates :field1, presence: true
|
|
200
231
|
validates :field2, presence: true # Won't run if field1 fails
|
|
201
|
-
validates :field3, presence: true # Won't run if
|
|
232
|
+
validates :field3, presence: true # Won't run if earlier fields fail
|
|
202
233
|
```
|
|
203
234
|
|
|
204
|
-
|
|
235
|
+
### ActiveModel Integration
|
|
205
236
|
|
|
206
237
|
Use ActiveModel's custom validation callbacks:
|
|
207
238
|
|
|
@@ -212,30 +243,90 @@ class CreateUser
|
|
|
212
243
|
|
|
213
244
|
params :user_data
|
|
214
245
|
|
|
215
|
-
validate :
|
|
246
|
+
validate :check_custom_logic
|
|
216
247
|
validates :username, presence: true
|
|
217
248
|
|
|
218
|
-
|
|
219
|
-
|
|
249
|
+
private
|
|
250
|
+
|
|
251
|
+
def check_custom_logic
|
|
252
|
+
errors.add(:base, "Custom validation failed") unless custom_condition?
|
|
220
253
|
end
|
|
221
254
|
end
|
|
222
255
|
```
|
|
223
256
|
|
|
257
|
+
### Performance Monitoring
|
|
258
|
+
|
|
259
|
+
Track validation performance in production:
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
config.enable_instrumentation = true
|
|
263
|
+
|
|
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
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Security
|
|
271
|
+
|
|
272
|
+
Built-in protection against common vulnerabilities:
|
|
273
|
+
|
|
274
|
+
### ReDoS Protection
|
|
275
|
+
|
|
276
|
+
Automatic timeouts prevent Regular Expression Denial of Service attacks:
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
config.regex_timeout = 0.1 # 100ms default
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
If a regex exceeds the timeout, validation fails safely instead of hanging.
|
|
283
|
+
|
|
284
|
+
### Memory Protection
|
|
285
|
+
|
|
286
|
+
Array size limits prevent memory exhaustion:
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
config.max_array_size = 1000 # Default limit
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Thread Safety
|
|
293
|
+
|
|
294
|
+
All validation operations are thread-safe for use with Puma, Sidekiq, etc.
|
|
295
|
+
|
|
296
|
+
### Best Practices
|
|
297
|
+
|
|
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
|
|
303
|
+
|
|
224
304
|
## Development
|
|
225
305
|
|
|
226
306
|
```bash
|
|
227
307
|
bin/setup # Install dependencies
|
|
228
|
-
bundle exec rspec # Run tests
|
|
308
|
+
bundle exec rspec # Run tests (231 examples)
|
|
229
309
|
bundle exec rubocop # Lint code
|
|
230
310
|
bin/console # Interactive console
|
|
231
311
|
```
|
|
232
312
|
|
|
313
|
+
### Benchmarking
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
bundle exec ruby benchmark/validation_benchmark.rb
|
|
317
|
+
```
|
|
318
|
+
|
|
233
319
|
## Requirements
|
|
234
320
|
|
|
235
321
|
- Ruby >= 3.2.0
|
|
236
322
|
- Interactor ~> 3.0
|
|
237
323
|
- ActiveModel >= 6.0
|
|
324
|
+
- ActiveSupport >= 6.0
|
|
238
325
|
|
|
239
326
|
## License
|
|
240
327
|
|
|
241
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)
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "benchmark/ips"
|
|
5
|
+
require "interactor"
|
|
6
|
+
require "interactor/validation"
|
|
7
|
+
|
|
8
|
+
# Benchmark different validation scenarios
|
|
9
|
+
puts "Interactor::Validation Performance Benchmarks"
|
|
10
|
+
puts "=" * 60
|
|
11
|
+
|
|
12
|
+
# Setup test interactors
|
|
13
|
+
class SimplePresenceInteractor
|
|
14
|
+
include Interactor
|
|
15
|
+
include Interactor::Validation
|
|
16
|
+
|
|
17
|
+
params :username, :email
|
|
18
|
+
|
|
19
|
+
validates :username, presence: true
|
|
20
|
+
validates :email, presence: true
|
|
21
|
+
|
|
22
|
+
def call; end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class ComplexValidationInteractor
|
|
26
|
+
include Interactor
|
|
27
|
+
include Interactor::Validation
|
|
28
|
+
|
|
29
|
+
params :username, :email, :age, :bio
|
|
30
|
+
|
|
31
|
+
validates :username, presence: true, length: { minimum: 3, maximum: 20 }
|
|
32
|
+
validates :email, presence: true, format: { with: /\A[\w+\-.]+@[a-z\d-]+(\.[a-z\d-]+)*\.[a-z]+\z/i }
|
|
33
|
+
validates :age, numericality: { greater_than: 0, less_than: 150 }
|
|
34
|
+
validates :bio, length: { maximum: 500 }
|
|
35
|
+
|
|
36
|
+
def call; end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class NestedValidationInteractor
|
|
40
|
+
include Interactor
|
|
41
|
+
include Interactor::Validation
|
|
42
|
+
|
|
43
|
+
params :user_data
|
|
44
|
+
|
|
45
|
+
validates :user_data do |v|
|
|
46
|
+
v.attribute :name, presence: true, length: { minimum: 2 }
|
|
47
|
+
v.attribute :email, presence: true, format: { with: /\A[\w+\-.]+@[a-z\d-]+(\.[a-z\d-]+)*\.[a-z]+\z/i }
|
|
48
|
+
v.attribute :age, numericality: { greater_than: 0 }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def call; end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class ArrayValidationInteractor
|
|
55
|
+
include Interactor
|
|
56
|
+
include Interactor::Validation
|
|
57
|
+
|
|
58
|
+
params :items
|
|
59
|
+
|
|
60
|
+
validates :items do |v|
|
|
61
|
+
v.attribute :name, presence: true
|
|
62
|
+
v.attribute :price, numericality: { greater_than: 0 }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def call; end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Valid test data
|
|
69
|
+
valid_simple_data = { username: "john_doe", email: "john@example.com" }
|
|
70
|
+
valid_complex_data = {
|
|
71
|
+
username: "john_doe",
|
|
72
|
+
email: "john@example.com",
|
|
73
|
+
age: 25,
|
|
74
|
+
bio: "Software developer"
|
|
75
|
+
}
|
|
76
|
+
valid_nested_data = {
|
|
77
|
+
user_data: {
|
|
78
|
+
name: "John Doe",
|
|
79
|
+
email: "john@example.com",
|
|
80
|
+
age: 25
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
valid_array_data = {
|
|
84
|
+
items: [
|
|
85
|
+
{ name: "Item 1", price: 10.99 },
|
|
86
|
+
{ name: "Item 2", price: 20.50 },
|
|
87
|
+
{ name: "Item 3", price: 15.75 }
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Invalid test data
|
|
92
|
+
invalid_simple_data = { username: "", email: "" }
|
|
93
|
+
|
|
94
|
+
# Benchmark: Simple presence validation
|
|
95
|
+
puts "\n1. Simple Presence Validation (2 fields)"
|
|
96
|
+
Benchmark.ips do |x|
|
|
97
|
+
x.config(time: 5, warmup: 2)
|
|
98
|
+
|
|
99
|
+
x.report("valid data") do
|
|
100
|
+
SimplePresenceInteractor.call(valid_simple_data)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
x.report("invalid data") do
|
|
104
|
+
result = SimplePresenceInteractor.call(invalid_simple_data)
|
|
105
|
+
result.failure?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
x.compare!
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Benchmark: Complex multi-rule validation
|
|
112
|
+
puts "\n2. Complex Validation (4 fields, multiple rules)"
|
|
113
|
+
Benchmark.ips do |x|
|
|
114
|
+
x.config(time: 5, warmup: 2)
|
|
115
|
+
|
|
116
|
+
x.report("valid data") do
|
|
117
|
+
ComplexValidationInteractor.call(valid_complex_data)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
x.compare!
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Benchmark: Nested validation
|
|
124
|
+
puts "\n3. Nested Hash Validation"
|
|
125
|
+
Benchmark.ips do |x|
|
|
126
|
+
x.config(time: 5, warmup: 2)
|
|
127
|
+
|
|
128
|
+
x.report("valid nested data") do
|
|
129
|
+
NestedValidationInteractor.call(valid_nested_data)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
x.compare!
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Benchmark: Array validation
|
|
136
|
+
puts "\n4. Array Validation (3 items)"
|
|
137
|
+
Benchmark.ips do |x|
|
|
138
|
+
x.config(time: 5, warmup: 2)
|
|
139
|
+
|
|
140
|
+
x.report("valid array") do
|
|
141
|
+
ArrayValidationInteractor.call(valid_array_data)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
x.compare!
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Benchmark: Regex caching impact
|
|
148
|
+
puts "\n5. Regex Pattern Caching (100 iterations)"
|
|
149
|
+
Benchmark.ips do |x|
|
|
150
|
+
x.config(time: 5, warmup: 2)
|
|
151
|
+
|
|
152
|
+
x.report("with caching") do
|
|
153
|
+
Interactor::Validation.configure { |c| c.cache_regex_patterns = true }
|
|
154
|
+
100.times { ComplexValidationInteractor.call(valid_complex_data) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
x.report("without caching") do
|
|
158
|
+
Interactor::Validation.configure { |c| c.cache_regex_patterns = false }
|
|
159
|
+
100.times { ComplexValidationInteractor.call(valid_complex_data) }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
x.compare!
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Benchmark: Halt on first error
|
|
166
|
+
puts "\n6. Halt on First Error (invalid data, 4 fields)"
|
|
167
|
+
invalid_all_data = { username: "", email: "invalid", age: -1, bio: "a" * 600 }
|
|
168
|
+
|
|
169
|
+
Benchmark.ips do |x|
|
|
170
|
+
x.config(time: 5, warmup: 2)
|
|
171
|
+
|
|
172
|
+
x.report("halt enabled") do
|
|
173
|
+
Interactor::Validation.configure { |c| c.halt_on_first_error = true }
|
|
174
|
+
ComplexValidationInteractor.call(invalid_all_data)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
x.report("halt disabled") do
|
|
178
|
+
Interactor::Validation.configure { |c| c.halt_on_first_error = false }
|
|
179
|
+
ComplexValidationInteractor.call(invalid_all_data)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
x.compare!
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Benchmark: Error mode comparison
|
|
186
|
+
puts "\n7. Error Formatting Mode"
|
|
187
|
+
Benchmark.ips do |x|
|
|
188
|
+
x.config(time: 5, warmup: 2)
|
|
189
|
+
|
|
190
|
+
x.report("code mode") do
|
|
191
|
+
Interactor::Validation.configure { |c| c.error_mode = :code }
|
|
192
|
+
SimplePresenceInteractor.call(invalid_simple_data)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
x.report("default mode") do
|
|
196
|
+
Interactor::Validation.configure { |c| c.error_mode = :default }
|
|
197
|
+
SimplePresenceInteractor.call(invalid_simple_data)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
x.compare!
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Benchmark: Large array validation
|
|
204
|
+
puts "\n8. Large Array Validation (100 items)"
|
|
205
|
+
large_array_data = {
|
|
206
|
+
items: Array.new(100) { |i| { name: "Item #{i}", price: rand(1.0..100.0).round(2) } }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
Benchmark.ips do |x|
|
|
210
|
+
x.config(time: 5, warmup: 2)
|
|
211
|
+
|
|
212
|
+
x.report("100 items") do
|
|
213
|
+
ArrayValidationInteractor.call(large_array_data)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
x.compare!
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Reset configuration
|
|
220
|
+
Interactor::Validation.reset_configuration!
|
|
221
|
+
|
|
222
|
+
puts "\n#{"=" * 60}"
|
|
223
|
+
puts "Benchmarks complete!"
|
|
224
|
+
puts "\nTo run these benchmarks:"
|
|
225
|
+
puts " bundle exec ruby benchmark/validation_benchmark.rb"
|
|
226
|
+
puts "\nTo add benchmark-ips to your Gemfile:"
|
|
227
|
+
puts " gem 'benchmark-ips', '~> 2.0', group: :development"
|