interactor-validation 0.2.0 → 0.3.1
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 +155 -130
- data/lib/interactor/validation/configuration.rb +3 -3
- data/lib/interactor/validation/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dcab1b0ef807a7e0c68d8c56570d7e3dd20337c0cb597015b2740132738c5c14
|
|
4
|
+
data.tar.gz: 82d5c575039217f7f6a90cb9a67677c980d74120caa59c7dc12e52f74688e056
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 553e1c172909fcb5dd3a16b00bfead612de5c1e46e95611014dd830d4bf46cd957c019613ed19c05775b236ef77e496b9d8a82a832badf731b74f63ff06a9ecc
|
|
7
|
+
data.tar.gz: 2382f09479bcc17d822113c3984c70ff341c7f583393ce6ee342c359aecb60139de92e01559934e84013e4a95368791b202b26eed314953df583be5f5cf5ef6d
|
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,136 +27,169 @@ 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
|
|
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
|
-
|
|
42
|
+
### Presence
|
|
64
43
|
|
|
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 mode
|
|
138
|
-
|
|
168
|
+
# Error format mode - Available options:
|
|
169
|
+
# :default - ActiveModel-style messages (default)
|
|
170
|
+
# { attribute: :email, type: :blank, message: "Email can't be blank" }
|
|
171
|
+
# :code - Structured error codes for APIs
|
|
172
|
+
# { code: "EMAIL_IS_REQUIRED" }
|
|
173
|
+
config.error_mode = :default
|
|
139
174
|
|
|
140
|
-
# Stop
|
|
175
|
+
# Stop at first error for better performance
|
|
141
176
|
config.halt_on_first_error = false
|
|
142
177
|
|
|
143
|
-
# Security
|
|
144
|
-
config.regex_timeout = 0.1
|
|
145
|
-
|
|
146
|
-
# Security: Maximum array size for nested validation (default: 1000)
|
|
147
|
-
config.max_array_size = 1000
|
|
178
|
+
# Security settings
|
|
179
|
+
config.regex_timeout = 0.1 # Regex timeout in seconds (ReDoS protection)
|
|
180
|
+
config.max_array_size = 1000 # Max array size for nested validation
|
|
148
181
|
|
|
149
|
-
# Performance
|
|
182
|
+
# Performance settings
|
|
150
183
|
config.cache_regex_patterns = true
|
|
151
184
|
|
|
152
|
-
# Monitoring
|
|
185
|
+
# Monitoring
|
|
153
186
|
config.enable_instrumentation = false
|
|
154
187
|
end
|
|
155
188
|
```
|
|
156
189
|
|
|
157
190
|
### Per-Interactor Configuration
|
|
158
191
|
|
|
159
|
-
Override
|
|
192
|
+
Override settings for specific interactors:
|
|
160
193
|
|
|
161
194
|
```ruby
|
|
162
195
|
class CreateUser
|
|
@@ -164,7 +197,7 @@ class CreateUser
|
|
|
164
197
|
include Interactor::Validation
|
|
165
198
|
|
|
166
199
|
configure_validation do |config|
|
|
167
|
-
config.error_mode = :
|
|
200
|
+
config.error_mode = :code
|
|
168
201
|
config.halt_on_first_error = true
|
|
169
202
|
end
|
|
170
203
|
|
|
@@ -173,47 +206,37 @@ class CreateUser
|
|
|
173
206
|
end
|
|
174
207
|
```
|
|
175
208
|
|
|
176
|
-
|
|
209
|
+
## Advanced Features
|
|
177
210
|
|
|
178
|
-
|
|
211
|
+
### Parameter Declaration
|
|
179
212
|
|
|
180
|
-
|
|
181
|
-
# With :code mode
|
|
182
|
-
configure_validation do |config|
|
|
183
|
-
config.error_mode = :code
|
|
184
|
-
end
|
|
213
|
+
Declare parameters for automatic delegation to context:
|
|
185
214
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
# => { code: "USERNAME_CUSTOM_REQUIRED_ERROR" }
|
|
189
|
-
# => { code: "EMAIL_CUSTOM_FORMAT_ERROR" }
|
|
215
|
+
```ruby
|
|
216
|
+
params :user_id, :action
|
|
190
217
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
218
|
+
def call
|
|
219
|
+
# Access directly instead of context.user_id
|
|
220
|
+
user = User.find(user_id)
|
|
221
|
+
user.perform(action)
|
|
194
222
|
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
223
|
```
|
|
199
224
|
|
|
200
|
-
###
|
|
201
|
-
|
|
202
|
-
#### Halt on First Error
|
|
225
|
+
### Halt on First Error
|
|
203
226
|
|
|
204
|
-
Improve performance by stopping validation
|
|
227
|
+
Improve performance by stopping validation early:
|
|
205
228
|
|
|
206
229
|
```ruby
|
|
207
230
|
configure_validation do |config|
|
|
208
|
-
config.halt_on_first_error = true
|
|
231
|
+
config.halt_on_first_error = true
|
|
209
232
|
end
|
|
210
233
|
|
|
211
234
|
validates :field1, presence: true
|
|
212
235
|
validates :field2, presence: true # Won't run if field1 fails
|
|
213
|
-
validates :field3, presence: true # Won't run if
|
|
236
|
+
validates :field3, presence: true # Won't run if earlier fields fail
|
|
214
237
|
```
|
|
215
238
|
|
|
216
|
-
|
|
239
|
+
### ActiveModel Integration
|
|
217
240
|
|
|
218
241
|
Use ActiveModel's custom validation callbacks:
|
|
219
242
|
|
|
@@ -224,88 +247,90 @@ class CreateUser
|
|
|
224
247
|
|
|
225
248
|
params :user_data
|
|
226
249
|
|
|
227
|
-
validate :
|
|
250
|
+
validate :check_custom_logic
|
|
228
251
|
validates :username, presence: true
|
|
229
252
|
|
|
230
|
-
|
|
231
|
-
|
|
253
|
+
private
|
|
254
|
+
|
|
255
|
+
def check_custom_logic
|
|
256
|
+
errors.add(:base, "Custom validation failed") unless custom_condition?
|
|
232
257
|
end
|
|
233
258
|
end
|
|
234
259
|
```
|
|
235
260
|
|
|
236
|
-
|
|
261
|
+
### Performance Monitoring
|
|
237
262
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
### ReDoS Protection (v0.2.0+)
|
|
241
|
-
|
|
242
|
-
Regular Expression Denial of Service attacks are prevented with automatic timeouts:
|
|
263
|
+
Track validation performance in production:
|
|
243
264
|
|
|
244
265
|
```ruby
|
|
245
|
-
config.
|
|
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:
|
|
266
|
+
config.enable_instrumentation = true
|
|
253
267
|
|
|
254
|
-
|
|
255
|
-
|
|
268
|
+
ActiveSupport::Notifications.subscribe('validate_params.interactor_validation') do |*args|
|
|
269
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
270
|
+
Rails.logger.info "Validation: #{event.duration}ms (#{event.payload[:interactor]})"
|
|
271
|
+
end
|
|
256
272
|
```
|
|
257
273
|
|
|
258
|
-
|
|
274
|
+
## Security
|
|
259
275
|
|
|
260
|
-
|
|
276
|
+
Built-in protection against common vulnerabilities:
|
|
261
277
|
|
|
262
|
-
###
|
|
278
|
+
### ReDoS Protection
|
|
263
279
|
|
|
264
|
-
|
|
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
|
|
280
|
+
Automatic timeouts prevent Regular Expression Denial of Service attacks:
|
|
268
281
|
|
|
269
|
-
|
|
282
|
+
```ruby
|
|
283
|
+
config.regex_timeout = 0.1 # 100ms default
|
|
284
|
+
```
|
|
270
285
|
|
|
271
|
-
|
|
286
|
+
If a regex exceeds the timeout, validation fails safely instead of hanging.
|
|
272
287
|
|
|
273
|
-
###
|
|
288
|
+
### Memory Protection
|
|
274
289
|
|
|
275
|
-
|
|
290
|
+
Array size limits prevent memory exhaustion:
|
|
276
291
|
|
|
277
|
-
```
|
|
278
|
-
|
|
292
|
+
```ruby
|
|
293
|
+
config.max_array_size = 1000 # Default limit
|
|
279
294
|
```
|
|
280
295
|
|
|
281
|
-
###
|
|
296
|
+
### Thread Safety
|
|
282
297
|
|
|
283
|
-
|
|
298
|
+
All validation operations are thread-safe for use with Puma, Sidekiq, etc.
|
|
284
299
|
|
|
285
|
-
|
|
286
|
-
config.enable_instrumentation = true
|
|
300
|
+
### Best Practices
|
|
287
301
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
302
|
+
- Use simple regex patterns (avoid nested quantifiers)
|
|
303
|
+
- Sanitize error messages before displaying in HTML
|
|
304
|
+
- Set appropriate `max_array_size` limits for your use case
|
|
305
|
+
- Enable instrumentation to monitor performance
|
|
306
|
+
- Review [SECURITY.md](SECURITY.md) for detailed information
|
|
293
307
|
|
|
294
308
|
## Development
|
|
295
309
|
|
|
296
310
|
```bash
|
|
297
311
|
bin/setup # Install dependencies
|
|
298
|
-
bundle exec rspec # Run tests
|
|
312
|
+
bundle exec rspec # Run tests (231 examples)
|
|
299
313
|
bundle exec rubocop # Lint code
|
|
300
314
|
bin/console # Interactive console
|
|
301
315
|
```
|
|
302
316
|
|
|
317
|
+
### Benchmarking
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
bundle exec ruby benchmark/validation_benchmark.rb
|
|
321
|
+
```
|
|
322
|
+
|
|
303
323
|
## Requirements
|
|
304
324
|
|
|
305
325
|
- Ruby >= 3.2.0
|
|
306
326
|
- Interactor ~> 3.0
|
|
307
327
|
- ActiveModel >= 6.0
|
|
328
|
+
- ActiveSupport >= 6.0
|
|
308
329
|
|
|
309
330
|
## License
|
|
310
331
|
|
|
311
332
|
MIT License - see [LICENSE.txt](LICENSE.txt)
|
|
333
|
+
|
|
334
|
+
## Contributing
|
|
335
|
+
|
|
336
|
+
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)
|
|
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 = :
|
|
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)
|