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 +4 -4
- data/README.md +151 -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: 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/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,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
|
|
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
|
|
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
|
|
142
173
|
|
|
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
|
|
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
|
|
178
|
+
# Performance settings
|
|
150
179
|
config.cache_regex_patterns = true
|
|
151
180
|
|
|
152
|
-
# Monitoring
|
|
181
|
+
# Monitoring
|
|
153
182
|
config.enable_instrumentation = false
|
|
154
183
|
end
|
|
155
184
|
```
|
|
156
185
|
|
|
157
186
|
### Per-Interactor Configuration
|
|
158
187
|
|
|
159
|
-
Override
|
|
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 = :
|
|
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
|
-
|
|
205
|
+
## Advanced Features
|
|
177
206
|
|
|
178
|
-
|
|
207
|
+
### Parameter Declaration
|
|
179
208
|
|
|
180
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
# => { code: "USERNAME_CUSTOM_REQUIRED_ERROR" }
|
|
189
|
-
# => { code: "EMAIL_CUSTOM_FORMAT_ERROR" }
|
|
211
|
+
```ruby
|
|
212
|
+
params :user_id, :action
|
|
190
213
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
###
|
|
201
|
-
|
|
202
|
-
#### Halt on First Error
|
|
221
|
+
### Halt on First Error
|
|
203
222
|
|
|
204
|
-
Improve performance by stopping validation
|
|
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
|
|
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
|
|
232
|
+
validates :field3, presence: true # Won't run if earlier fields fail
|
|
214
233
|
```
|
|
215
234
|
|
|
216
|
-
|
|
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 :
|
|
246
|
+
validate :check_custom_logic
|
|
228
247
|
validates :username, presence: true
|
|
229
248
|
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
257
|
+
### Performance Monitoring
|
|
237
258
|
|
|
238
|
-
|
|
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.
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
|
|
270
|
+
## Security
|
|
259
271
|
|
|
260
|
-
|
|
272
|
+
Built-in protection against common vulnerabilities:
|
|
261
273
|
|
|
262
|
-
###
|
|
274
|
+
### ReDoS Protection
|
|
263
275
|
|
|
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
|
|
276
|
+
Automatic timeouts prevent Regular Expression Denial of Service attacks:
|
|
268
277
|
|
|
269
|
-
|
|
278
|
+
```ruby
|
|
279
|
+
config.regex_timeout = 0.1 # 100ms default
|
|
280
|
+
```
|
|
270
281
|
|
|
271
|
-
|
|
282
|
+
If a regex exceeds the timeout, validation fails safely instead of hanging.
|
|
272
283
|
|
|
273
|
-
###
|
|
284
|
+
### Memory Protection
|
|
274
285
|
|
|
275
|
-
|
|
286
|
+
Array size limits prevent memory exhaustion:
|
|
276
287
|
|
|
277
|
-
```
|
|
278
|
-
|
|
288
|
+
```ruby
|
|
289
|
+
config.max_array_size = 1000 # Default limit
|
|
279
290
|
```
|
|
280
291
|
|
|
281
|
-
###
|
|
292
|
+
### Thread Safety
|
|
282
293
|
|
|
283
|
-
|
|
294
|
+
All validation operations are thread-safe for use with Puma, Sidekiq, etc.
|
|
284
295
|
|
|
285
|
-
|
|
286
|
-
config.enable_instrumentation = true
|
|
296
|
+
### Best Practices
|
|
287
297
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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)
|
|
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)
|