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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc137c0e8cdc24b828f1e2e1369f4527908b48d328df61e994e64ad736dbf59e
4
- data.tar.gz: b5c1bddd3dd946662b1394bbd5126aa8361798b59743f9f0da661bcf37166f37
3
+ metadata.gz: dcab1b0ef807a7e0c68d8c56570d7e3dd20337c0cb597015b2740132738c5c14
4
+ data.tar.gz: 82d5c575039217f7f6a90cb9a67677c980d74120caa59c7dc12e52f74688e056
5
5
  SHA512:
6
- metadata.gz: be6bb4b7fe4580208395dd82989f00de651b08bb90a5de35197307d2c9e4f2caff0b41b508a2b3ed973919fdf7bc03aa0c8a953cba9ea86dacc17ea5885cbb4e
7
- data.tar.gz: 14128318e687a03b13dc1bd0c3144f2db560ec2f2ed039289ee3c87d4726f5784720a72184d7693cbf55180b43782e405a7282f52e1b4a8f4311048991993bcf
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 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,169 @@ 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 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 validation at first error (default: false)
175
+ # Stop at first error for better performance
141
176
  config.halt_on_first_error = false
142
177
 
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
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: Cache compiled regex patterns (default: true)
182
+ # Performance settings
150
183
  config.cache_regex_patterns = true
151
184
 
152
- # Monitoring: Enable ActiveSupport::Notifications (default: false)
185
+ # Monitoring
153
186
  config.enable_instrumentation = false
154
187
  end
155
188
  ```
156
189
 
157
190
  ### Per-Interactor Configuration
158
191
 
159
- Override global settings for specific interactors:
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 = :default
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
- ### Custom Error Messages
209
+ ## Advanced Features
177
210
 
178
- Provide custom error messages for any validation:
211
+ ### Parameter Declaration
179
212
 
180
- ```ruby
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
- 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" }
215
+ ```ruby
216
+ params :user_id, :action
190
217
 
191
- # With :default mode
192
- configure_validation do |config|
193
- config.error_mode = :default
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
- ### Advanced Usage
201
-
202
- #### Halt on First Error
225
+ ### Halt on First Error
203
226
 
204
- Improve performance by stopping validation at the first failure:
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 # Stop at first error
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 field1 or field2 fails
236
+ validates :field3, presence: true # Won't run if earlier fields fail
214
237
  ```
215
238
 
216
- #### Integration with ActiveModel Validations
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 :check_user_data_structure
250
+ validate :check_custom_logic
228
251
  validates :username, presence: true
229
252
 
230
- def check_user_data_structure
231
- errors.add(:user_data, "must be a Hash") unless user_data.is_a?(Hash)
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
- ## Security
261
+ ### Performance Monitoring
237
262
 
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:
263
+ Track validation performance in production:
243
264
 
244
265
  ```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:
266
+ config.enable_instrumentation = true
253
267
 
254
- ```ruby
255
- config.max_array_size = 1000 # Default limit
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
- ### Thread Safety (v0.2.0+)
274
+ ## Security
259
275
 
260
- Validation rule registration is thread-safe and can be used safely in multi-threaded environments (Puma, Sidekiq).
276
+ Built-in protection against common vulnerabilities:
261
277
 
262
- ### Best Practices
278
+ ### ReDoS Protection
263
279
 
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
280
+ Automatic timeouts prevent Regular Expression Denial of Service attacks:
268
281
 
269
- For detailed security information, see [SECURITY.md](SECURITY.md).
282
+ ```ruby
283
+ config.regex_timeout = 0.1 # 100ms default
284
+ ```
270
285
 
271
- ## Performance
286
+ If a regex exceeds the timeout, validation fails safely instead of hanging.
272
287
 
273
- ### Benchmarking
288
+ ### Memory Protection
274
289
 
275
- Run the included benchmark suite to measure performance:
290
+ Array size limits prevent memory exhaustion:
276
291
 
277
- ```bash
278
- bundle exec ruby benchmark/validation_benchmark.rb
292
+ ```ruby
293
+ config.max_array_size = 1000 # Default limit
279
294
  ```
280
295
 
281
- ### Monitoring
296
+ ### Thread Safety
282
297
 
283
- Enable instrumentation to track validation performance in production:
298
+ All validation operations are thread-safe for use with Puma, Sidekiq, etc.
284
299
 
285
- ```ruby
286
- config.enable_instrumentation = true
300
+ ### Best Practices
287
301
 
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
- ```
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) [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.1"
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wilson Anciro