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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d6d255c1cdb4048fbd485a9bc8b95096d521e592e6babe4ea7961ec429312386
4
- data.tar.gz: a9bc28cb3c10f5f2415da43c3a63b8c774436e13d27e2e6facaaa0cbcbeb19b9
3
+ metadata.gz: 337f8200b44c78cb5a2eeefa3d9dcd5c5c1ec864ef7f0e18d710309365b21e9f
4
+ data.tar.gz: 4c0b74a6a2cc0fbc9bd391665da07a454054c7ec29fef12d84dccd6945296ba3
5
5
  SHA512:
6
- metadata.gz: f5e73c0e35a659668dd31783d5fbea0869f54efe3db2d1033320c66d90fe9fb7997a2fc7079b602608ac6aa1bec60467588ba4b6fdea6bdd3b890cd24ea55bf5
7
- data.tar.gz: 7da53549b4bd1ef65065b9480dca243f1a78922045f096281025f7e35a034c8418ce88c90cac698fd6dd8816a9c434e23050b88a42701d96803e2bcf73d8f78a
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
- **Hash Validation:**
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
- # Usage
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
- # Hash validation
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
- # Usage
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
- # Usage
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
- validate_nested(param_name, value, rules[:_nested])
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
- # Note: halt flag is set by ErrorsWrapper.add() if halt: true
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
- # Note: halt flag is set by ErrorsWrapper.add() if halt: true
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Interactor
4
4
  module Validation
5
- VERSION = "0.3.7"
5
+ VERSION = "0.3.8"
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.3.7
4
+ version: 0.3.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wilson Anciro