interactor-validation 0.3.7 → 0.3.8
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 +118 -9
- data/lib/interactor/validation/validates.rb +19 -6
- 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: 337f8200b44c78cb5a2eeefa3d9dcd5c5c1ec864ef7f0e18d710309365b21e9f
|
|
4
|
+
data.tar.gz: 4c0b74a6a2cc0fbc9bd391665da07a454054c7ec29fef12d84dccd6945296ba3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b3fc4532523214b0723c009ed73b2d75ad5213480ae920261da7263adf2b3929ecef608b1d6b8924461f0fc873cf11777a2f80ea32528a9c8228320025086440
|
|
7
|
+
data.tar.gz: f58f08c52ef1e58be7d8ddb566d14b8aff8b24c408f1976128decada786ce29570ac38d6df7a862e50f5681e64e3ed9eb73b65fb6909da80504f3b113f28c06b
|
data/README.md
CHANGED
|
@@ -213,19 +213,49 @@ validates :terms_accepted, boolean: true
|
|
|
213
213
|
|
|
214
214
|
### Nested Validation
|
|
215
215
|
|
|
216
|
-
Validate nested hashes and arrays.
|
|
216
|
+
Validate nested hashes and arrays with support for both optional and required parameters.
|
|
217
217
|
|
|
218
|
-
**
|
|
218
|
+
**Optional Nested Validation (parameter can be nil):**
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
params :filters
|
|
222
|
+
validates :filters do
|
|
223
|
+
attribute :type, presence: true
|
|
224
|
+
attribute :value
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# When filters is nil or missing - succeeds
|
|
228
|
+
result = SearchItems.call(filters: nil)
|
|
229
|
+
result.success? # => true
|
|
230
|
+
|
|
231
|
+
# When filters is present - validates nested attributes
|
|
232
|
+
result = SearchItems.call(filters: { type: "category" })
|
|
233
|
+
result.success? # => true
|
|
234
|
+
|
|
235
|
+
# When filters is present but invalid - fails
|
|
236
|
+
result = SearchItems.call(filters: { value: "test" })
|
|
237
|
+
result.errors # => [{ attribute: "filters.type", type: :blank, message: "Filters.type can't be blank" }]
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Required Nested Validation (parameter must be present):**
|
|
219
241
|
|
|
220
242
|
```ruby
|
|
221
243
|
params :user
|
|
222
|
-
validates :user do
|
|
244
|
+
validates :user, presence: true do
|
|
223
245
|
attribute :name, presence: true
|
|
224
246
|
attribute :email, format: { with: /@/ }
|
|
225
247
|
attribute :age, numericality: { greater_than: 0 }
|
|
226
248
|
end
|
|
227
249
|
|
|
228
|
-
#
|
|
250
|
+
# When user is nil - fails with presence error
|
|
251
|
+
result = CreateUser.call(user: nil)
|
|
252
|
+
result.errors # => [{ attribute: :user, type: :blank, message: "User can't be blank" }]
|
|
253
|
+
|
|
254
|
+
# When user is empty hash - fails with presence error (empty hashes are not .present?)
|
|
255
|
+
result = CreateUser.call(user: {})
|
|
256
|
+
result.errors # => [{ attribute: :user, type: :blank, message: "User can't be blank" }]
|
|
257
|
+
|
|
258
|
+
# When user is present - validates nested attributes
|
|
229
259
|
result = CreateUser.call(user: { name: "", email: "bad", age: -1 })
|
|
230
260
|
result.errors # => [
|
|
231
261
|
# { attribute: "user.name", type: :blank, message: "User.name can't be blank" },
|
|
@@ -252,6 +282,18 @@ result.errors # => [
|
|
|
252
282
|
# { attribute: "items[1].name", type: :blank, message: "Items[1].name can't be blank" },
|
|
253
283
|
# { attribute: "items[1].price", type: :greater_than, message: "Items[1].price must be greater than 0" }
|
|
254
284
|
# ]
|
|
285
|
+
|
|
286
|
+
# Optional array (can be nil)
|
|
287
|
+
result = ProcessItems.call(items: nil)
|
|
288
|
+
result.success? # => true
|
|
289
|
+
|
|
290
|
+
# Required array (must be present)
|
|
291
|
+
validates :items, presence: true do
|
|
292
|
+
attribute :name, presence: true
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
result = ProcessItems.call(items: nil)
|
|
296
|
+
result.errors # => [{ attribute: :items, type: :blank, message: "Items can't be blank" }]
|
|
255
297
|
```
|
|
256
298
|
|
|
257
299
|
---
|
|
@@ -1059,14 +1101,45 @@ end
|
|
|
1059
1101
|
### Nested Validation Examples
|
|
1060
1102
|
|
|
1061
1103
|
```ruby
|
|
1062
|
-
#
|
|
1104
|
+
# Optional hash validation (parameter can be nil)
|
|
1105
|
+
class SearchWithFilters
|
|
1106
|
+
include Interactor
|
|
1107
|
+
include Interactor::Validation
|
|
1108
|
+
|
|
1109
|
+
params :filters
|
|
1110
|
+
|
|
1111
|
+
validates :filters do
|
|
1112
|
+
attribute :category, presence: true
|
|
1113
|
+
attribute :min_price, numericality: { greater_than_or_equal_to: 0 }
|
|
1114
|
+
attribute :max_price, numericality: { greater_than_or_equal_to: 0 }
|
|
1115
|
+
end
|
|
1116
|
+
|
|
1117
|
+
def call
|
|
1118
|
+
# filters is optional - if nil, skip filtering
|
|
1119
|
+
results = filters ? apply_filters(Product.all) : Product.all
|
|
1120
|
+
context.results = results
|
|
1121
|
+
end
|
|
1122
|
+
end
|
|
1123
|
+
|
|
1124
|
+
# When filters is nil - succeeds
|
|
1125
|
+
result = SearchWithFilters.call(filters: nil)
|
|
1126
|
+
result.success? # => true
|
|
1127
|
+
|
|
1128
|
+
# When filters is present but invalid - fails
|
|
1129
|
+
result = SearchWithFilters.call(filters: { min_price: -10 })
|
|
1130
|
+
result.errors # => [
|
|
1131
|
+
# { attribute: "filters.category", type: :blank, message: "Filters.category can't be blank" },
|
|
1132
|
+
# { attribute: "filters.min_price", type: :greater_than_or_equal_to, message: "..." }
|
|
1133
|
+
# ]
|
|
1134
|
+
|
|
1135
|
+
# Required hash validation (parameter must be present)
|
|
1063
1136
|
class CreateUserWithProfile
|
|
1064
1137
|
include Interactor
|
|
1065
1138
|
include Interactor::Validation
|
|
1066
1139
|
|
|
1067
1140
|
params :user
|
|
1068
1141
|
|
|
1069
|
-
validates :user do
|
|
1142
|
+
validates :user, presence: true do
|
|
1070
1143
|
attribute :name, presence: true
|
|
1071
1144
|
attribute :email, format: { with: /@/ }
|
|
1072
1145
|
attribute :age, numericality: { greater_than: 0 }
|
|
@@ -1078,7 +1151,14 @@ class CreateUserWithProfile
|
|
|
1078
1151
|
end
|
|
1079
1152
|
end
|
|
1080
1153
|
|
|
1081
|
-
#
|
|
1154
|
+
# When user is nil or empty - fails with presence error
|
|
1155
|
+
result = CreateUserWithProfile.call(user: nil)
|
|
1156
|
+
result.errors # => [{ attribute: :user, type: :blank, message: "User can't be blank" }]
|
|
1157
|
+
|
|
1158
|
+
result = CreateUserWithProfile.call(user: {})
|
|
1159
|
+
result.errors # => [{ attribute: :user, type: :blank, message: "User can't be blank" }]
|
|
1160
|
+
|
|
1161
|
+
# When user is present but invalid - validates nested attributes
|
|
1082
1162
|
result = CreateUserWithProfile.call(
|
|
1083
1163
|
user: {
|
|
1084
1164
|
name: "",
|
|
@@ -1094,7 +1174,7 @@ result.errors # => [
|
|
|
1094
1174
|
# { attribute: "user.bio", type: :too_long, message: "User.bio is too long (maximum is 500 characters)" }
|
|
1095
1175
|
# ]
|
|
1096
1176
|
|
|
1097
|
-
# Array validation
|
|
1177
|
+
# Array validation (optional)
|
|
1098
1178
|
class BulkCreateItems
|
|
1099
1179
|
include Interactor
|
|
1100
1180
|
include Interactor::Validation
|
|
@@ -1108,11 +1188,16 @@ class BulkCreateItems
|
|
|
1108
1188
|
end
|
|
1109
1189
|
|
|
1110
1190
|
def call
|
|
1191
|
+
return if items.nil? # Optional - handle nil gracefully
|
|
1111
1192
|
items.each { |item| Item.create!(item) }
|
|
1112
1193
|
end
|
|
1113
1194
|
end
|
|
1114
1195
|
|
|
1115
|
-
#
|
|
1196
|
+
# When items is nil - succeeds
|
|
1197
|
+
result = BulkCreateItems.call(items: nil)
|
|
1198
|
+
result.success? # => true
|
|
1199
|
+
|
|
1200
|
+
# When items is present - validates each item
|
|
1116
1201
|
result = BulkCreateItems.call(
|
|
1117
1202
|
items: [
|
|
1118
1203
|
{ name: "Widget", price: 10, quantity: 5 },
|
|
@@ -1124,6 +1209,30 @@ result.errors # => [
|
|
|
1124
1209
|
# { attribute: "items[1].price", type: :greater_than, message: "Items[1].price must be greater than 0" },
|
|
1125
1210
|
# { attribute: "items[1].quantity", type: :greater_than_or_equal_to, message: "Items[1].quantity must be greater than or equal to 1" }
|
|
1126
1211
|
# ]
|
|
1212
|
+
|
|
1213
|
+
# Required array validation
|
|
1214
|
+
class BulkCreateRequiredItems
|
|
1215
|
+
include Interactor
|
|
1216
|
+
include Interactor::Validation
|
|
1217
|
+
|
|
1218
|
+
params :items
|
|
1219
|
+
|
|
1220
|
+
validates :items, presence: true do
|
|
1221
|
+
attribute :name, presence: true
|
|
1222
|
+
attribute :price, numericality: { greater_than: 0 }
|
|
1223
|
+
end
|
|
1224
|
+
|
|
1225
|
+
def call
|
|
1226
|
+
items.each { |item| Item.create!(item) }
|
|
1227
|
+
end
|
|
1228
|
+
end
|
|
1229
|
+
|
|
1230
|
+
# When items is nil or empty - fails with presence error
|
|
1231
|
+
result = BulkCreateRequiredItems.call(items: nil)
|
|
1232
|
+
result.errors # => [{ attribute: :items, type: :blank, message: "Items can't be blank" }]
|
|
1233
|
+
|
|
1234
|
+
result = BulkCreateRequiredItems.call(items: [])
|
|
1235
|
+
result.errors # => [{ attribute: :items, type: :blank, message: "Items can't be blank" }]
|
|
1127
1236
|
```
|
|
1128
1237
|
|
|
1129
1238
|
### ActiveModel Integration
|
|
@@ -46,8 +46,9 @@ module Interactor
|
|
|
46
46
|
nested_rules = build_nested_rules(&)
|
|
47
47
|
current_validations = _param_validations.dup
|
|
48
48
|
existing_rules = current_validations[param_name] || {}
|
|
49
|
+
# Merge both the validation rules (like presence: true) AND the nested rules
|
|
49
50
|
self._param_validations = current_validations.merge(
|
|
50
|
-
param_name => existing_rules.merge(_nested: nested_rules)
|
|
51
|
+
param_name => existing_rules.merge(rules).merge(_nested: nested_rules)
|
|
51
52
|
)
|
|
52
53
|
return
|
|
53
54
|
end
|
|
@@ -260,14 +261,23 @@ module Interactor
|
|
|
260
261
|
end
|
|
261
262
|
|
|
262
263
|
# Validates a single parameter with the given rules
|
|
263
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
264
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
264
265
|
def validate_param(param_name, value, rules)
|
|
265
266
|
# Skip validation if explicitly marked
|
|
266
267
|
return if rules[:_skip]
|
|
267
268
|
|
|
268
269
|
# Handle nested validation (hash or array)
|
|
269
270
|
if rules[:_nested]
|
|
270
|
-
|
|
271
|
+
# Run presence validation first if it exists
|
|
272
|
+
# This allows optional vs required nested validation
|
|
273
|
+
validate_presence(param_name, value, rules)
|
|
274
|
+
return if @halt_validation || (@current_config.halt && errors.any?)
|
|
275
|
+
|
|
276
|
+
# Only run nested validation if value is present (not nil and not empty)
|
|
277
|
+
# If parent has no presence requirement, treat empty hash/array as "not provided"
|
|
278
|
+
# If parent has presence: true, empty hash/array already failed presence check above
|
|
279
|
+
should_validate_nested = value.present? || value == false
|
|
280
|
+
validate_nested(param_name, value, rules[:_nested]) if should_validate_nested
|
|
271
281
|
return
|
|
272
282
|
end
|
|
273
283
|
|
|
@@ -289,10 +299,13 @@ module Interactor
|
|
|
289
299
|
|
|
290
300
|
validate_numericality(param_name, value, rules)
|
|
291
301
|
end
|
|
292
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
302
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
293
303
|
|
|
294
304
|
# Validates nested attributes in a hash or array
|
|
295
305
|
def validate_nested(param_name, value, nested_rules)
|
|
306
|
+
# Return early if value is nil - presence validation handles this if required
|
|
307
|
+
return if value.nil?
|
|
308
|
+
|
|
296
309
|
if value.is_a?(Array)
|
|
297
310
|
validate_array_of_hashes(param_name, value, nested_rules)
|
|
298
311
|
elsif value.is_a?(Hash)
|
|
@@ -501,7 +514,7 @@ module Interactor
|
|
|
501
514
|
errors.add(attribute_path, error_type, halt: halt, **interpolations)
|
|
502
515
|
end
|
|
503
516
|
|
|
504
|
-
#
|
|
517
|
+
# NOTE: halt flag is set by ErrorsWrapper.add() if halt: true
|
|
505
518
|
end
|
|
506
519
|
# rubocop:enable Metrics/ParameterLists
|
|
507
520
|
|
|
@@ -708,7 +721,7 @@ module Interactor
|
|
|
708
721
|
errors.add(param_name, error_type, halt: halt, **interpolations)
|
|
709
722
|
end
|
|
710
723
|
|
|
711
|
-
#
|
|
724
|
+
# NOTE: halt flag is set by ErrorsWrapper.add() if halt: true
|
|
712
725
|
end
|
|
713
726
|
|
|
714
727
|
# Generate error code for :code mode using constants
|