interactor_support 1.0.6 → 1.0.7

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: bf8cda13999c9971a1499398e23135ef6e870af7581ffe7e9f173b9d7c4231ea
4
- data.tar.gz: 48c401b5b42e80ccea3db170ecb1e8476861522804b800ad28651eebfa674222
3
+ metadata.gz: 2a9492a85b640343b10a985ca038fb2a2bbcd98a5532ae61f0b98abdb41106e9
4
+ data.tar.gz: 29bfa393df505cb275b0deea347b39c012e3e3af2f6f01610f99c1b639c3e0f1
5
5
  SHA512:
6
- metadata.gz: 2adf5f317e6432571a24358e9e1ba453397fcfd5f6b83758930fd45bef652dfb01ead87aa68fcfec385151bd60646127a087968e332baa0193e5bb9f7ac14ff6
7
- data.tar.gz: a6a670cacb4abd50812a82232dcbeb1e582eea72d3526b2659f4dccd827226c6e07c6b12f6c39b282eb276db482d02911cd4d0e125094f8c1d114fba5459c899
6
+ metadata.gz: 14d5ce85f9e66aaaeb0df8f23fe1de9cb21b3dabfe9a8c4bd7b4a072a9a8ec4400d05ae4402b09a73a0304e20c33cfd00a50c3188c874d1f8b3399c39729089a
7
+ data.tar.gz: 32159eb9b275c2c4aac8125d00e608836670b8775faf8daab3caa3e78abf8b63d17bdaa7eb6018c8882fb7ab41ea02a4389c72f856ba16f69e5c342a2158239c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.0.7] - 2025-09-24
4
+
5
+ - Add `handle_interactor_failure` DSL and per-call overrides for centralized interactor failure handling
6
+ - Raise an internal failure signal so handled responses automatically halt controller actions (with opt-out via `halt_on_handle: false`)
7
+ - Provide `InteractorSupport.configuration.default_interactor_error_handler` for global handler registration
8
+ - Expose richer failure payloads (context, error, request object, params) to handlers
9
+
10
+ ## [1.0.6] - 2025-09-24
11
+
12
+ - Wrap request object validation failures from `Organizable#organize` in `InteractorSupport::Errors::InvalidRequestObject` for consistent controller handling
13
+ - Honor `configuration.log_unknown_request_object_attributes` when logging ignored request object keys
14
+ - Improve request object error messaging for failed casts and unknown attributes
15
+
3
16
  ## [1.0.0] - 2025-03-20
4
17
 
5
18
  - Initial release
@@ -29,9 +42,3 @@
29
42
  - Introduce `InteractorSupport.configuration.logger` and `log_level` for customizable logging
30
43
  - Override `assign_attributes` to integrate attribute ignoring and error-raising behavior
31
44
  - Improve test coverage for unknown attribute handling and logging
32
-
33
- ## [1.0.6] - 2025-09-24
34
-
35
- - Wrap request object validation failures from `Organizable#organize` in `InteractorSupport::Errors::InvalidRequestObject` for consistent controller handling
36
- - Honor `configuration.log_unknown_request_object_attributes` when logging ignored request object keys
37
- - Improve request object error messaging for failed casts and unknown attributes
data/README.md CHANGED
@@ -575,6 +575,8 @@ Calls the given interactor with a request object built from the provided params.
575
575
  | params | Hash | Parameters passed to the request object. |
576
576
  | request_object | Class | A request object class that accepts params in its initializer. |
577
577
  | context_key | Symbol or nil | Optional key to namespace the request object inside the interactor context. |
578
+ | error_handler | Symbol, Proc, Array, `false`, `nil` | Override the failure handlers for this call. Use `false` to skip handlers or include `:defaults` to inject registered ones. |
579
+ | halt_on_handle | Boolean | Whether to halt the caller when a handler marks the failure handled (defaults to `true`). |
578
580
 
579
581
  Examples
580
582
 
@@ -588,18 +590,43 @@ organize(MyInteractor, params: request_params, request_object: MyRequest, contex
588
590
  # => MyInteractor.call({ request: MyRequest.new(params) })
589
591
  ```
590
592
 
591
- Validation failures inside the request object raise `InteractorSupport::Errors::InvalidRequestObject`. The exception exposes the request class and its validation messages, making it straightforward to surface errors back to the caller.
593
+ If you opt out of failure handlers, validation errors still bubble as `InteractorSupport::Errors::InvalidRequestObject`. Registering a handler with `handle_interactor_failure` lets you centralise the response instead:
592
594
 
593
595
  ```rb
594
- def create
595
- organize(CreateUserInteractor, params: request_params(:user), request_object: CreateUserRequest)
596
- redirect_to dashboard_path
597
- rescue InteractorSupport::Errors::InvalidRequestObject => e
598
- flash.now[:alert] = "Unable to continue: #{e.errors.to_sentence}"
599
- render :new, status: :unprocessable_entity
596
+ class UsersController < ApplicationController
597
+ include InteractorSupport::Concerns::Organizable
598
+
599
+ handle_interactor_failure :render_failure
600
+
601
+ def create
602
+ organize(CreateUserInteractor,
603
+ params: request_params(:user),
604
+ request_object: CreateUserRequest)
605
+
606
+ render json: @context.user, status: :created
607
+ end
608
+
609
+ private
610
+
611
+ def render_failure(failure)
612
+ flash.now[:alert] = failure.errors.to_sentence
613
+ render :new, status: :unprocessable_entity
614
+ failure.handled!
615
+ end
600
616
  end
601
617
  ```
602
618
 
619
+ When the handler calls `failure.handled!` (or returns truthy), the concern raises an internal signal that is swallowed by Rails’ `rescue_from`, halting the action before the success response runs. Pass `halt_on_handle: false` to `organize` for cases where you still want the action to continue.
620
+
621
+ You can override handlers per call with the `error_handler:` option. Symbols/Procs are accepted, and the special token `:defaults` injects any class-level or globally configured handlers.
622
+
623
+ ##### Failure handler options
624
+
625
+ - `handle_interactor_failure :method, only: [:create], except: [:destroy]` — scope handlers to particular actions.
626
+ - `organize(..., error_handler: false)` — skip all registered handlers for a single call.
627
+ - `organize(..., error_handler: [:audit_failure, :defaults])` — prepend custom handlers while still running the defaults.
628
+ - `organize(..., halt_on_handle: false)` — allow logic after `organize` to run even if a handler reported the failure handled.
629
+
603
630
  #### #request_params(\*top_level_keys, merge: {}, except: [], rewrite: [])
604
631
 
605
632
  Returns a shaped parameter hash derived from params.permit!. You can extract specific top-level keys, rename them, flatten values, apply defaults, and remove unwanted fields.
@@ -704,6 +731,7 @@ All global settings for InteractorSupport can be set via the `InteractorSupport.
704
731
  | `log_unknown_request_object_attributes` | `Boolean` | `true` | Whether to log unknown request attributes that are ignored. |
705
732
  | `request_object_behavior` | `Symbol` | `:returns_context` | Controls what `RequestObject.new(...)` returns (`:returns_self` or `:returns_context`). |
706
733
  | `request_object_key_type` | `Symbol` | `:symbol` | Controls the output format of keys in `#to_context` (`:symbol`, `:string`, `:struct`). |
734
+ | `default_interactor_error_handler` | `Symbol`, `Proc`, `Array` | `nil` | Global failure handler(s) invoked when `handle_interactor_failure` is not used or when `:defaults` is requested. |
707
735
  <!-- prettier-ignore-end -->
708
736
 
709
737
  To update these settings, use:
@@ -715,6 +743,7 @@ InteractorSupport.configure do |config|
715
743
  config.log_unknown_request_object_attributes = true
716
744
  config.request_object_behavior = :returns_self
717
745
  config.request_object_key_type = :struct
746
+ config.default_interactor_error_handler = :render_global_failure
718
747
  end
719
748
  ```
720
749
 
data/Rakefile CHANGED
@@ -1,10 +1,10 @@
1
1
  # filepath: ./Rakefile
2
2
  # frozen_string_literal: true
3
3
 
4
- require "bundler/gem_tasks"
5
- require "rails"
6
- require "rspec/core/rake_task"
7
- require "rubocop/rake_task"
4
+ require 'bundler/gem_tasks'
5
+ require 'rails'
6
+ require 'rspec/core/rake_task'
7
+ require 'rubocop/rake_task'
8
8
  require 'active_record'
9
9
  require 'rake'
10
10
  require 'yaml'
@@ -12,7 +12,7 @@ require 'yaml'
12
12
  RSpec::Core::RakeTask.new(:spec)
13
13
  RuboCop::RakeTask.new
14
14
 
15
- task default: %i[spec rubocop]
15
+ task default: [:spec, :rubocop]
16
16
 
17
17
  # Load the Rails environment
18
18
  ENV['RAILS_ENV'] ||= 'test'
@@ -25,27 +25,27 @@ ActiveRecord::Tasks::DatabaseTasks.migrations_paths = [File.expand_path('spec/mi
25
25
 
26
26
  # Load the ActiveRecord tasks manually
27
27
  namespace :db do
28
- desc "Migrate the database"
28
+ desc 'Migrate the database'
29
29
  task :migrate do
30
30
  ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths
31
31
  ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths).migrate
32
32
  end
33
33
 
34
- desc "Create the database"
34
+ desc 'Create the database'
35
35
  task :create do
36
36
  ActiveRecord::Tasks::DatabaseTasks.create_current
37
37
  end
38
38
 
39
- desc "Drop the database"
39
+ desc 'Drop the database'
40
40
  task :drop do
41
41
  ActiveRecord::Tasks::DatabaseTasks.drop_current
42
42
  end
43
43
 
44
- desc "Reset the database"
45
- task :reset => [:drop, :create, :migrate]
44
+ desc 'Reset the database'
45
+ task reset: [:drop, :create, :migrate]
46
46
  end
47
47
 
48
48
  # Load the Rake tasks
49
49
  Rake::Task.define_task(:environment) do
50
50
  ActiveRecord::Base.establish_connection(:test)
51
- end
51
+ end
@@ -1,11 +1,9 @@
1
1
  # lib/interactor_support/version.rb
2
2
  module InteractorSupport
3
3
  ##
4
- # A bundle of DSL-style concerns that enhance interactors with expressive,
5
- # composable behavior.
4
+ # Bundles the most common InteractorSupport concerns into a single include.
6
5
  #
7
- # This module is intended to be included into an `Interactor` or `Organizer`,
8
- # providing access to a suite of declarative action helpers:
6
+ # Mix this into an `Interactor` or `Interactor::Organizer` to gain access to:
9
7
  #
10
8
  # - {InteractorSupport::Concerns::Skippable} — Conditionally skip execution
11
9
  # - {InteractorSupport::Concerns::Transactionable} — Wrap logic in an ActiveRecord transaction
@@ -1,12 +1,12 @@
1
1
  module InteractorSupport
2
2
  module Concerns
3
3
  ##
4
- # Adds dynamic model-finding helpers (`find_by`, `find_where`) to an interactor.
4
+ # DSL helpers for loading records into context before an interactor runs.
5
5
  #
6
- # This concern wraps ActiveRecord `.find_by` and `.where` queries into
7
- # declarative DSL methods that load records into the interactor context.
6
+ # - `find_by` loads a single record via `.find_by`, using context values or lambdas for conditions.
7
+ # - `find_where` loads collections via `.where`, `.where.not`, and optional scopes.
8
8
  #
9
- # These methods support symbols (for context keys) and lambdas (for dynamic runtime evaluation).
9
+ # Use `required: true` to fail the context automatically when nothing is found.
10
10
  #
11
11
  # @example Find a post by ID from the context
12
12
  # find_by :post
@@ -27,10 +27,10 @@ module InteractorSupport
27
27
 
28
28
  included do
29
29
  class << self
30
- # Adds a `before` callback to find a single record and assign it to context.
30
+ # Adds a `before` callback to find a single record and assign it to the context.
31
31
  #
32
- # This method searches for a record based on the provided query parameters.
33
- # It supports dynamic values using symbols (context keys) and lambdas (for runtime evaluation).
32
+ # Symbols pull values from the context, lambdas run via `instance_exec`, and literal values are
33
+ # passed directly to `find_by`. When `query` is omitted, the DSL defaults to `<model>_id`.
34
34
  #
35
35
  # @param model [Symbol, String] the name of the model to query (e.g., `:post`)
36
36
  # @param query [Hash{Symbol=>Object,Proc}] a hash of attributes to match (can use symbols for context lookup or lambdas)
@@ -69,10 +69,10 @@ module InteractorSupport
69
69
  end
70
70
  end
71
71
 
72
- # Adds a `before` callback to find multiple records and assign them to context.
72
+ # Adds a `before` callback to find multiple records (or relations) and assign them to context.
73
73
  #
74
- # This method performs a `.where` query with optional `.where.not` and `.scope`,
75
- # allowing dynamic values using symbols (for context lookup) and lambdas (for runtime evaluation).
74
+ # Supports `.where`, `.where.not`, and optional scopes. Symbols pull from context, lambdas run via
75
+ # `instance_exec`, enabling flexible query composition.
76
76
  #
77
77
  # @param model [Symbol, String] the name of the model to query (e.g., `:post`)
78
78
  # @param where [Hash{Symbol=>Object,Proc}] conditions for `.where` (can use symbols or lambdas)
@@ -3,71 +3,220 @@
3
3
  module InteractorSupport
4
4
  module Concerns
5
5
  ##
6
- # The `Organizable` module provides utility methods for organizing interactors
7
- # and shaping request parameters in a structured way.
6
+ # Utilities for invoking interactors with request objects and shaping incoming params.
8
7
  #
9
- # It is intended to be included into a controller or a base service class that
10
- # delegates to interactors using request objects.
8
+ # Include this concern in controllers or service entry points to:
9
+ # - Allowlist and transform request parameters in a single place
10
+ # - Build request objects and pass them to interactors with one call
11
+ # - Receive consistent `InvalidRequestObject` errors when validation fails
12
+ # - Register reusable failure handlers with {#handle_interactor_failure}
11
13
  #
12
14
  # @example Include in a controller
13
15
  # class ApplicationController < ActionController::Base
14
16
  # include InteractorSupport::Organizable
15
17
  # end
16
18
  #
17
- # @see InteractorSupport::Organizable#organize
18
- # @see InteractorSupport::Organizable#request_params
19
+ # @see InteractorSupport::Concerns::Organizable#organize
20
+ # @see InteractorSupport::Concerns::Organizable#request_params
21
+ # @see InteractorSupport::Concerns::Organizable.handle_interactor_failure
19
22
  module Organizable
20
23
  include ActiveSupport::Concern
21
24
 
22
- # Calls the given interactor with a request object.
23
- # Optionally wraps the request object under a key in the interactor context.
25
+ FailureHandledSignal = Class.new(StandardError) do
26
+ attr_reader :failure
27
+
28
+ def initialize(failure)
29
+ @failure = failure
30
+ super('Interactor failure handled')
31
+ end
32
+ end
33
+
34
+ FailurePayload = Struct.new(
35
+ :context,
36
+ :error,
37
+ :interactor,
38
+ :request_object,
39
+ :params,
40
+ :controller,
41
+ keyword_init: true,
42
+ ) do
43
+ def handled!
44
+ @handled = true
45
+ end
46
+
47
+ def handled?
48
+ !!@handled
49
+ end
50
+
51
+ def errors
52
+ if error.respond_to?(:errors) && error.errors.present?
53
+ error.errors
54
+ elsif context.respond_to?(:errors) && context.errors.present?
55
+ context.errors
56
+ elsif error.respond_to?(:message)
57
+ Array(error.message).compact
58
+ else
59
+ []
60
+ end
61
+ end
62
+
63
+ def status
64
+ if error.respond_to?(:status)
65
+ error.status
66
+ elsif context.respond_to?(:status)
67
+ context.status
68
+ end
69
+ end
70
+
71
+ def to_h
72
+ {
73
+ context: context,
74
+ error: error,
75
+ interactor: interactor,
76
+ request_object: request_object,
77
+ params: params,
78
+ handled: handled?,
79
+ }
80
+ end
81
+ end
82
+
83
+ ErrorHandlerDefinition = Struct.new(:callable, :only, :except, keyword_init: true) do
84
+ def applicable?(action_name)
85
+ action = action_name&.to_sym
86
+ return false if only.any? && action && !only.include?(action)
87
+ return false if except.any? && action && except.include?(action)
88
+
89
+ true
90
+ end
91
+ end
92
+
93
+ def self.included(base)
94
+ super
95
+
96
+ base.extend(ClassMethods)
97
+
98
+ if base.respond_to?(:rescue_from)
99
+ base.rescue_from(FailureHandledSignal) do |_signal|
100
+ # Handled responses are already rendered by the registered handler.
101
+ end
102
+ end
103
+ end
104
+
105
+ module ClassMethods
106
+ ##
107
+ # Registers a failure handler that runs when an interactor fails.
108
+ #
109
+ # Handlers execute in declaration order. Use `only:` and `except:` to scope execution
110
+ # to specific actions (mirroring Rails filters). A handler may be a method name,
111
+ # Proc, or any callable responding to `#call`.
112
+ #
113
+ # Returning a truthy value or calling `failure.handled!` marks the failure as handled.
114
+ #
115
+ # @param handler [Symbol, Proc, #call] the handler to invoke
116
+ # @param only [Array<Symbol>, Symbol, nil] optional list of actions to run on
117
+ # @param except [Array<Symbol>, Symbol, nil] optional list of actions to skip
118
+ # @return [void]
119
+ def handle_interactor_failure(handler, only: nil, except: nil)
120
+ definition = ErrorHandlerDefinition.new(
121
+ callable: handler,
122
+ only: Array(only).compact.map(&:to_sym),
123
+ except: Array(except).compact.map(&:to_sym),
124
+ )
125
+
126
+ definitions = interactor_failure_handler_definitions + [definition]
127
+ @_interactor_failure_handler_definitions = definitions.freeze
128
+ end
129
+
130
+ def interactor_failure_handler_definitions
131
+ @_interactor_failure_handler_definitions ||= []
132
+ end
133
+
134
+ def reset_interactor_failure_handlers!
135
+ @_interactor_failure_handler_definitions = [].freeze
136
+ end
137
+ end
138
+
139
+ # Calls the given interactor with a request object derived from `params`.
140
+ #
141
+ # - If `context_key` is provided, the request is namespaced under that key when invoking `call`.
142
+ # - Validation failures raise {InteractorSupport::Errors::InvalidRequestObject}, allowing the caller
143
+ # to rescue and render validation messages without inspecting ActiveModel internals.
24
144
  #
25
- # @param interactor [Class] The interactor class to call.
26
- # @param params [Hash] Parameters to initialize the request object.
27
- # @param request_object [Class] A request object class that responds to `#new(params)`.
145
+ # @param interactor [Class] The interactor class or organizer to call.
146
+ # @param params [Hash] Raw parameters to initialize the request object.
147
+ # @param request_object [Class] A request object class that responds to `.new`.
28
148
  # @param context_key [Symbol, nil] Optional key to assign the request object under in the context.
149
+ # @param error_handler [Symbol, Proc, Array<Symbol,Proc>, false, nil] Override the registered failure handlers.
150
+ # Use `false` to skip handlers, or include `:defaults` inside the array to inject class/config handlers.
151
+ # @param halt_on_handle [Boolean] When true (default), a handled failure halts the caller via an internal signal.
29
152
  #
30
- # @return [void]
153
+ # @return [Interactor::Context]
31
154
  #
32
- # @example
33
- # organize(MyInteractor, params: request_params, request_object: MyRequest)
34
- # # => Calls MyInteractor with an instance of MyRequest initialized with request_params.
155
+ # @example Basic call
156
+ # organize(Users::Create, params: request_params(:user), request_object: CreateUserRequest)
35
157
  #
36
- # @example
37
- # organize(MyInteractor, params: request_params, request_object: MyRequest, context_key: :request)
38
- # # => Calls MyInteractor with an instance of MyRequest initialized with request_params at :context_key.
39
- # # # => The context will contain { request: MyRequest.new(request_params) }
40
- def organize(interactor, params:, request_object:, context_key: nil)
41
- request_payload = request_object.new(params)
158
+ # @example Namespace the request in context
159
+ # organize(Users::Create,
160
+ # params: request_params(:user),
161
+ # request_object: CreateUserRequest,
162
+ # context_key: :request)
163
+ def organize(interactor, params:, request_object:, context_key: nil, error_handler: nil, halt_on_handle: true)
164
+ @_interactor_failure_handled = false
165
+ @context = nil
42
166
 
43
- @context = interactor.call(
44
- context_key ? { context_key => request_payload } : request_payload,
45
- )
46
- rescue ActiveModel::ValidationError => e
47
- errors =
48
- if e.model&.respond_to?(:errors)
49
- e.model.errors.full_messages
50
- else
51
- []
52
- end
167
+ handlers = resolve_error_handlers(error_handler)
53
168
 
54
- raise InteractorSupport::Errors::InvalidRequestObject.new(
55
- request_class: request_object,
56
- errors: errors,
169
+ request_payload = build_request_payload(
170
+ interactor: interactor,
171
+ request_object: request_object,
172
+ params: params,
173
+ handlers: handlers,
174
+ halt_on_handle: halt_on_handle,
57
175
  )
176
+
177
+ return @context if interactor_failure_handled?
178
+
179
+ payload = context_key ? { context_key => request_payload } : request_payload
180
+
181
+ @context = invoke_interactor(interactor, payload, handlers, request_object, params, halt_on_handle)
182
+
183
+ if failure_context?(@context)
184
+ failure = dispatch_interactor_failure_handlers(
185
+ handlers: handlers,
186
+ context: @context,
187
+ error: extract_context_error(@context),
188
+ interactor: interactor,
189
+ request_object: request_object,
190
+ params: params,
191
+ )
192
+
193
+ emit_failure_signal_if_needed(failure, halt_on_handle)
194
+ end
195
+
196
+ @context
197
+ end
198
+
199
+ ##
200
+ # Indicates whether the most recent `organize` call routed a failure through a handler.
201
+ # Useful when you pass `halt_on_handle: false` and want to branch manually.
202
+ #
203
+ # @return [Boolean]
204
+ def interactor_failure_handled?
205
+ !!@_interactor_failure_handled
58
206
  end
59
207
 
60
- # Builds a structured and optionally transformed parameter hash from Rails' `params`.
208
+ # Builds a structured parameter hash from Rails' `params`, with helpers for rewriting keys.
61
209
  #
62
- # This method supports extracting specific top-level keys, applying optional rewrite
63
- # transformations, merging in additional values, and excluding unwanted keys.
210
+ # Use this as the single entry point for shaping incoming parameters before they are given to
211
+ # request objects. It combines extraction, filtering, renaming, flattening, defaults, and merges
212
+ # in a single call.
64
213
  #
65
214
  # @param top_level_keys [Array<Symbol>] Top-level keys to extract from `params`. If empty, all keys are included.
66
215
  # @param merge [Hash] Additional values to merge into the final result.
67
216
  # @param except [Array<Symbol, Array<Symbol>>] Keys or nested key paths to exclude from the result.
68
217
  # @param rewrite [Array<Hash>] A set of transformation rules applied to the top-level keys.
69
218
  #
70
- # @return [Hash] The final, shaped parameters hash.
219
+ # @return [Hash] The shaped parameters hash ready for request object initialization.
71
220
  #
72
221
  # @example Extracting a specific top-level key
73
222
  # # Given: params = { order: { product_id: 1, quantity: 2 } }
@@ -203,6 +352,180 @@ module InteractorSupport
203
352
 
204
353
  duped
205
354
  end
355
+
356
+ def build_request_payload(interactor:, request_object:, params:, handlers:, halt_on_handle:)
357
+ request_object.new(params)
358
+ rescue ActiveModel::ValidationError => e
359
+ invalid_error = InteractorSupport::Errors::InvalidRequestObject.new(
360
+ request_class: request_object,
361
+ errors: extract_active_model_errors(e),
362
+ )
363
+
364
+ failure = dispatch_interactor_failure_handlers(
365
+ handlers: handlers,
366
+ context: nil,
367
+ error: invalid_error,
368
+ interactor: interactor,
369
+ request_object: request_object,
370
+ params: params,
371
+ )
372
+
373
+ emit_failure_signal_if_needed(failure, halt_on_handle)
374
+
375
+ raise invalid_error unless failure.handled?
376
+
377
+ @context
378
+ rescue FailureHandledSignal
379
+ raise
380
+ rescue StandardError => e
381
+ failure = dispatch_interactor_failure_handlers(
382
+ handlers: handlers,
383
+ context: nil,
384
+ error: e,
385
+ interactor: interactor,
386
+ request_object: request_object,
387
+ params: params,
388
+ )
389
+
390
+ emit_failure_signal_if_needed(failure, halt_on_handle)
391
+
392
+ raise e unless failure.handled?
393
+
394
+ @context
395
+ end
396
+
397
+ def invoke_interactor(interactor, payload, handlers, request_object, params, halt_on_handle)
398
+ context = interactor.call(payload)
399
+ @context = context
400
+ context
401
+ rescue FailureHandledSignal
402
+ raise
403
+ rescue StandardError => e
404
+ failure = dispatch_interactor_failure_handlers(
405
+ handlers: handlers,
406
+ context: nil,
407
+ error: e,
408
+ interactor: interactor,
409
+ request_object: request_object,
410
+ params: params,
411
+ )
412
+
413
+ emit_failure_signal_if_needed(failure, halt_on_handle)
414
+
415
+ raise e unless failure.handled?
416
+
417
+ @context
418
+ end
419
+
420
+ def extract_active_model_errors(exception)
421
+ if exception.model&.respond_to?(:errors)
422
+ exception.model.errors.full_messages
423
+ else
424
+ []
425
+ end
426
+ end
427
+
428
+ def extract_context_error(context)
429
+ if context.respond_to?(:error)
430
+ context.error
431
+ elsif context.respond_to?(:errors) && context.errors.respond_to?(:full_messages)
432
+ context.errors
433
+ end
434
+ end
435
+
436
+ def failure_context?(context)
437
+ context.respond_to?(:failure?) && context.failure?
438
+ end
439
+
440
+ def dispatch_interactor_failure_handlers(handlers:, context:, error:, interactor:, request_object:, params:)
441
+ failure = FailurePayload.new(
442
+ context: context,
443
+ error: error,
444
+ interactor: interactor,
445
+ request_object: request_object,
446
+ params: params,
447
+ controller: self,
448
+ )
449
+
450
+ return failure if handlers.empty?
451
+
452
+ action_scope = current_interactor_action
453
+
454
+ handlers.each do |definition|
455
+ next unless definition.applicable?(action_scope)
456
+
457
+ invoke_handler(definition.callable, failure)
458
+ end
459
+
460
+ failure
461
+ end
462
+
463
+ def invoke_handler(handler, failure)
464
+ result =
465
+ case handler
466
+ when Proc
467
+ handler.arity.zero? ? instance_exec(&handler) : instance_exec(failure, &handler)
468
+ when Symbol, String
469
+ callable_method = method(handler)
470
+ callable_method.arity.zero? ? callable_method.call : callable_method.call(failure)
471
+ else
472
+ if handler.respond_to?(:call)
473
+ call_arity = handler.respond_to?(:arity) ? handler.arity : nil
474
+ call_arity&.zero? ? handler.call : handler.call(failure)
475
+ else
476
+ raise ArgumentError, "Interactor failure handler #{handler.inspect} is not callable"
477
+ end
478
+ end
479
+
480
+ failure.handled! if result && !failure.handled?
481
+
482
+ failure.handled?
483
+ end
484
+
485
+ def emit_failure_signal_if_needed(failure, halt_on_handle)
486
+ return unless failure.handled?
487
+
488
+ @_interactor_failure_handled = true
489
+
490
+ return unless halt_on_handle
491
+
492
+ if respond_to?(:rescue_with_handler)
493
+ raise FailureHandledSignal.new(failure)
494
+ end
495
+ end
496
+
497
+ def resolve_error_handlers(custom)
498
+ case custom
499
+ when false
500
+ []
501
+ when nil
502
+ class_handler_definitions + configuration_handler_definitions
503
+ else
504
+ Array(custom).flat_map do |item|
505
+ if item == :defaults || item == :default
506
+ class_handler_definitions + configuration_handler_definitions
507
+ else
508
+ ErrorHandlerDefinition.new(callable: item, only: [], except: [])
509
+ end
510
+ end
511
+ end
512
+ end
513
+
514
+ def class_handler_definitions
515
+ Array(self.class.interactor_failure_handler_definitions).dup
516
+ end
517
+
518
+ def configuration_handler_definitions
519
+ Array(InteractorSupport.configuration.default_interactor_error_handler).compact.map do |handler|
520
+ ErrorHandlerDefinition.new(callable: handler, only: [], except: [])
521
+ end
522
+ end
523
+
524
+ def current_interactor_action
525
+ return action_name.to_sym if respond_to?(:action_name) && action_name
526
+
527
+ nil
528
+ end
206
529
  end
207
530
  end
208
531
  end
@@ -3,14 +3,13 @@
3
3
  module InteractorSupport
4
4
  module Concerns
5
5
  ##
6
- # Adds a DSL method to conditionally skip an interactor.
6
+ # Adds a declarative `skip` helper for short-circuiting interactor execution.
7
7
  #
8
- # This concern provides a `skip` method that wraps the interactor in an `around` block.
9
- # You can pass an `:if` or `:unless` condition using a Proc, Symbol, or literal.
10
- # The condition will be evaluated at runtime to determine whether to run the interactor.
8
+ # Conditions run inside an `around` callback, accepting symbols, booleans, or lambdas executed via
9
+ # `instance_exec`. Use this to prevent unnecessary work when preconditions fail.
11
10
  #
12
- # - Symbols will be looked up on the interactor or in the context.
13
- # - Lambdas/Procs are evaluated using `instance_exec` with full access to context.
11
+ # - Symbols first look for an interactor instance method, then fall back to context values.
12
+ # - Lambdas have full access to the interactor instance and context.
14
13
  #
15
14
  # @example Skip if the user is already authenticated (symbol in context)
16
15
  # skip if: :user_authenticated
@@ -31,10 +30,7 @@ module InteractorSupport
31
30
  ##
32
31
  # Skips the interactor based on a condition provided via `:if` or `:unless`.
33
32
  #
34
- # This wraps the interactor in an `around` hook, and conditionally skips
35
- # execution based on truthy/falsy evaluation of the provided options.
36
- #
37
- # The condition can be a Proc (evaluated in context), a Symbol (used to call a method or context key), or a literal value.
33
+ # A truthy `:if` or falsy `:unless` prevents `call` from running; otherwise execution continues.
38
34
  #
39
35
  # @param options [Hash]
40
36
  # @option options [Proc, Symbol, Boolean] :if a condition that must be truthy to skip
@@ -1,13 +1,11 @@
1
1
  module InteractorSupport
2
2
  module Concerns
3
3
  ##
4
- # Adds transactional support to your interactor using ActiveRecord.
4
+ # Adds a declarative `transaction` wrapper around interactor execution using ActiveRecord.
5
5
  #
6
- # The `transaction` method wraps the interactor execution in an `around` block
7
- # that uses `ActiveRecord::Base.transaction`. If the context fails (via `context.fail!`),
8
- # the transaction is rolled back automatically using `ActiveRecord::Rollback`.
9
- #
10
- # This is useful for ensuring your interactor behaves atomically.
6
+ # Enabling the wrapper ensures that:
7
+ # - The interactor runs inside `ActiveRecord::Base.transaction` with configurable options.
8
+ # - `context.fail!` triggers an `ActiveRecord::Rollback` so partial work is undone.
11
9
  #
12
10
  # @example Basic usage
13
11
  # class CreateUser
@@ -31,12 +29,12 @@ module InteractorSupport
31
29
  class << self
32
30
  # Wraps the interactor in a database transaction.
33
31
  #
34
- # If the context fails (`context.failure?`), a rollback is triggered automatically.
35
- # You can customize the transaction behavior using standard ActiveRecord options.
32
+ # If the context fails (`context.failure?`), a rollback is triggered automatically. Supports
33
+ # the same keyword options as `ActiveRecord::Base.transaction`.
36
34
  #
37
- # @param isolation [Symbol, nil] the transaction isolation level (e.g., `:read_committed`, `:serializable`)
35
+ # @param isolation [Symbol, nil] optional transaction isolation level (e.g., `:read_committed`)
38
36
  # @param joinable [Boolean] whether this transaction can join an existing one
39
- # @param requires_new [Boolean] whether to force a new transaction, even if one already exists
37
+ # @param requires_new [Boolean] whether to force a new transaction even if one already exists
40
38
  #
41
39
  # @example Wrap in a basic transaction
42
40
  # transaction
@@ -1,10 +1,10 @@
1
1
  module InteractorSupport
2
2
  module Concerns
3
3
  ##
4
- # Adds helpers for assigning and transforming values in interactor context.
4
+ # Adds helpers for preparing context data before the interactor `call` executes.
5
5
  #
6
- # The `context_variable` method sets static or dynamic values before the interactor runs.
7
- # The `transform` method allows chaining transformations (methods or lambdas) on context values.
6
+ # - `context_variable` seeds the context with static values or lazily evaluated lambdas.
7
+ # - `transform` normalizes existing context values using symbols, lambdas, or chains of both.
8
8
  #
9
9
  # @example Assign context variables before the interactor runs
10
10
  # context_variable user: -> { User.find(user_id) }, numbers: [1, 2, 3]
@@ -27,10 +27,10 @@ module InteractorSupport
27
27
  class << self
28
28
  # Assigns one or more values to the context before the interactor runs.
29
29
  #
30
- # Values can be static or lazily evaluated with a lambda/proc using `instance_exec`,
31
- # which provides access to the context and interactor instance.
30
+ # Values can be static or lazily evaluated with a proc using `instance_exec`, which has
31
+ # access to the interactor instance and context.
32
32
  #
33
- # @param key_values [Hash{Symbol => Object, Proc}] a mapping of context keys to values or Procs
33
+ # @param key_values [Hash{Symbol => Object, Proc}] mapping of context keys to values or Procs
34
34
  #
35
35
  # @example Static and dynamic values
36
36
  # context_variable first_post: Post.first
@@ -48,15 +48,17 @@ module InteractorSupport
48
48
  end
49
49
  end
50
50
 
51
- # Transforms one or more context values using a method, a proc, or a sequence of methods.
51
+ # Transforms one or more context values using symbols, procs, or chains of both.
52
52
  #
53
- # This allows simple transformations like `:strip` or `:downcase`, or more complex lambdas.
54
- # You can also chain transformations by passing an array of method names.
53
+ # - Symbols call the method on the current value (e.g., `:strip`).
54
+ # - Procs run via `instance_exec`, so they can reach other context values.
55
+ # - Arrays allow combining multiple operations in order.
55
56
  #
56
- # If a transformation fails, the context fails with an error message.
57
+ # Any transformation failure uses `context.fail!` with a helpful error message so the
58
+ # interactor halts gracefully.
57
59
  #
58
60
  # @param keys [Array<Symbol>] one or more context keys to transform
59
- # @param with [Symbol, Array<Symbol>, Proc] a single method name, an array of method names, or a proc
61
+ # @param with [Symbol, Array<Symbol, Proc>, Proc] method name(s) or a proc used to transform values
60
62
  #
61
63
  # @raise [ArgumentError] if no keys are given, or if an invalid `with:` value is passed
62
64
  #
@@ -1,13 +1,16 @@
1
1
  module InteractorSupport
2
2
  module Concerns
3
3
  ##
4
- # Adds an `update` DSL method for updating a context-loaded model with attributes.
4
+ # Adds an `update` DSL for synchronizing context data back into ActiveRecord models.
5
5
  #
6
- # This concern allows flexible updates using data from the interactor's context.
7
- # It supports direct mapping from context keys, nested attribute extraction from parent objects,
8
- # lambdas for dynamic evaluation, or passing a symbol pointing to an entire context object.
6
+ # The DSL supports:
7
+ # - Direct mappings from context keys to model attributes
8
+ # - Plucking nested values from other context objects (hashes or structs)
9
+ # - Lambdas for dynamic evaluation, executed in the interactor context
10
+ # - Passing a symbol that points to a hash of attributes in the context (mass assignment)
9
11
  #
10
- # This is useful for updating records cleanly and consistently in declarative steps.
12
+ # Each update runs before `#call` and uses `record.update!`, so failures raise immediately unless
13
+ # rescued by the interactor.
11
14
  #
12
15
  # @example Update a user using context values
13
16
  # update :user, attributes: { name: :new_name, email: :new_email }
@@ -27,15 +30,18 @@ module InteractorSupport
27
30
  class << self
28
31
  # Updates a model using values from the context before the interactor runs.
29
32
  #
30
- # Supports flexible ways of specifying attributes:
31
- # - A hash mapping attribute names to context keys, nested keys, or lambdas
32
- # - A symbol pointing to a hash in context
33
+ # - When `attributes` is a Hash, keys are written to the record. Values can be:
34
+ # * Symbols (looked up on the context)
35
+ # * Arrays (pluck multiple keys from another context object)
36
+ # * Hashes (extract values from a parent context object)
37
+ # * Procs (executed with `instance_exec` for custom logic)
38
+ # - When `attributes` is a Symbol, the corresponding context hash is used directly.
33
39
  #
34
- # If the record or required data is missing, the context fails with an error.
40
+ # Missing data triggers `context.fail!` with a helpful message so the update halts cleanly.
35
41
  #
36
- # @param model [Symbol] the key in the context holding the record to update
37
- # @param attributes [Hash, Symbol] a hash mapping attributes to context keys/lambdas, or a symbol pointing to a context hash
38
- # @param context_key [Symbol, nil] key to assign the updated record to in context (defaults to `model`)
42
+ # @param model [Symbol] context key for the record to update
43
+ # @param attributes [Hash, Symbol] mapping of target attributes or context hash to copy from
44
+ # @param context_key [Symbol, nil] context key to store the updated record (defaults to `model`)
39
45
  #
40
46
  # @example Basic attribute update using context keys
41
47
  # update :user, attributes: { name: :new_name, email: :new_email }
@@ -1,8 +1,8 @@
1
1
  module InteractorSupport
2
2
  ##
3
- # Global configuration for InteractorSupport.
3
+ # Global configuration entry point for InteractorSupport.
4
4
  #
5
- # This allows customization of how request objects behave when used in interactors.
5
+ # Use this to tailor request object behavior, logging, and context conversion.
6
6
  #
7
7
  # @example Set custom behavior
8
8
  # InteractorSupport.configuration.request_object_behavior = :returns_self
@@ -45,6 +45,11 @@ module InteractorSupport
45
45
  # @see InteractorSupport::RequestObject#ignore_unknown_attributes
46
46
  attr_accessor :log_unknown_request_object_attributes
47
47
 
48
+ ##
49
+ # Default interactor failure handler(s) applied when none are specified.
50
+ # Accepts a symbol, callable, or array of either.
51
+ attr_accessor :default_interactor_error_handler
52
+
48
53
  ##
49
54
  # Initializes the configuration with default values:
50
55
  # - `request_object_behavior` defaults to `:returns_context`
@@ -58,6 +63,7 @@ module InteractorSupport
58
63
  @logger = Logger.new($stdout)
59
64
  @log_level = Logger::INFO
60
65
  @log_unknown_request_object_attributes = true
66
+ @default_interactor_error_handler = nil
61
67
  end
62
68
  end
63
69
  end
@@ -1,10 +1,9 @@
1
1
  module InteractorSupport
2
2
  ##
3
- # Core behavior that ensures the `Interactor` module is included
4
- # when any InteractorSupport concern is mixed in.
3
+ # Core hook that ensures the `Interactor` module is present when using InteractorSupport concerns.
5
4
  #
6
- # This module is automatically included by all `InteractorSupport::Concerns`,
7
- # so you generally do not need to include it manually.
5
+ # This module is automatically included by all `InteractorSupport::Concerns` so, in practice, you do
6
+ # not need to include it yourself.
8
7
  #
9
8
  # @example Included implicitly
10
9
  # class MyInteractor
@@ -1,4 +1,6 @@
1
1
  module InteractorSupport
2
+ ##
3
+ # Custom error types surfaced by InteractorSupport helpers.
2
4
  module Errors
3
5
  class UnknownAttribute < StandardError
4
6
  attr_reader :attribute, :owner
@@ -47,7 +49,7 @@ module InteractorSupport
47
49
  request_class.to_s
48
50
  end
49
51
 
50
- detail = @errors.any? ? ": #{@errors.join(', ')}" : ''
52
+ detail = @errors.any? ? ": #{@errors.join(", ")}" : ''
51
53
 
52
54
  super("Invalid #{request_name}#{detail}")
53
55
  end
@@ -1,10 +1,16 @@
1
1
  module InteractorSupport
2
2
  ##
3
- # A base module for building validated, transformable, and optionally nested request objects.
3
+ # Provides a concise DSL for building validated, transformable, and nested request objects.
4
4
  #
5
- # It builds on top of `ActiveModel::Model`, adds coercion, default values, attribute transforms,
6
- # key rewriting, and automatic context conversion (via `#to_context`). It integrates tightly with
7
- # `InteractorSupport::Configuration` to control return behavior and key formatting.
5
+ # Including this module gives you:
6
+ # - ActiveModel validations and callbacks
7
+ # - Attribute coercion (via ActiveModel types or custom classes/request objects)
8
+ # - Value transforms, defaults, and key rewriting
9
+ # - A `#to_context` helper that converts the object into hashes or structs for interactors
10
+ # - Predictable error handling (unknown attributes raise {Errors::UnknownAttribute},
11
+ # invalid records raise {ActiveModel::ValidationError})
12
+ #
13
+ # Configure return semantics (hash vs. struct vs. self) through {InteractorSupport::Configuration}.
8
14
  #
9
15
  # @example Basic usage
10
16
  # class CreateUserRequest
@@ -13,16 +19,19 @@ module InteractorSupport
13
19
  # attribute :name, transform: [:strip, :downcase]
14
20
  # attribute :email
15
21
  # attribute :metadata, default: {}
22
+ #
23
+ # validates :email, presence: true
16
24
  # end
17
25
  #
18
26
  # CreateUserRequest.new(name: " JOHN ", email: "hi@example.com")
19
27
  # # => { name: "john", email: "hi@example.com", metadata: {} }
20
28
  #
21
- # @example Key rewriting
29
+ # @example Key rewriting and nested objects
22
30
  # class UploadRequest
23
31
  # include InteractorSupport::RequestObject
24
32
  #
25
33
  # attribute :image, rewrite: :image_url
34
+ # attribute :metadata, type: ImageMetadataRequest
26
35
  # end
27
36
  #
28
37
  # UploadRequest.new(image: "url").image_url # => "url"
@@ -44,9 +53,11 @@ module InteractorSupport
44
53
  include ActiveModel::Validations::Callbacks
45
54
 
46
55
  ##
47
- # Initializes the request object and raises if invalid.
56
+ # Initializes the request object, applying key rewrites and validations.
48
57
  #
49
- # Rewritten keys are converted before passing to ActiveModel.
58
+ # Unknown keys trigger {Errors::UnknownAttribute} (unless `ignore_unknown_attributes` is enabled).
59
+ # Validation failures raise `ActiveModel::ValidationError`, which is wrapped by
60
+ # {InteractorSupport::Concerns::Organizable#organize organize} when used through organizers.
50
61
  #
51
62
  # @param attributes [Hash] the input attributes
52
63
  # @raise [ActiveModel::ValidationError] if the object is invalid
@@ -63,12 +74,12 @@ module InteractorSupport
63
74
  end
64
75
 
65
76
  ##
66
- # Converts the request object into a format suitable for interactor context.
77
+ # Converts the request object into the structure expected by interactors.
67
78
  #
68
- # - If `key_type` is `:symbol` or `:string`, returns a Hash.
69
- # - If `key_type` is `:struct`, returns a Struct instance.
79
+ # - If `request_object_key_type` is `:symbol` or `:string`, returns a Hash keyed accordingly.
80
+ # - If `request_object_key_type` is `:struct`, returns a Struct with attribute readers.
70
81
  #
71
- # Nested request objects will also be converted recursively.
82
+ # Nested request objects (including arrays of request objects) are converted recursively.
72
83
  #
73
84
  # @return [Hash, Struct]
74
85
  def to_context
@@ -90,14 +101,14 @@ module InteractorSupport
90
101
  end
91
102
 
92
103
  ##
93
- # Assigns the given attributes to the request object.
104
+ # Assigns external attributes, respecting rewrite rules and the unknown-attribute policy.
94
105
  #
95
- # - Known attributes are assigned normally via their setters.
96
- # - If `ignore_unknown_attributes?` is defined and true, unknown keys are ignored and logged.
97
- # - Otherwise, raises `Errors::UnknownAttribute`.
106
+ # - Known attributes are routed through generated setters so transforms and type coercion run.
107
+ # - If `ignore_unknown_attributes` was declared, unrecognized keys are ignored (and optionally logged).
108
+ # - Otherwise {Errors::UnknownAttribute} is raised with the offending key and request class.
98
109
  #
99
110
  # @param attrs [Hash] input attributes to assign
100
- # @raise [Errors::UnknownAttribute] if unknown attribute is encountered and not ignored
111
+ # @raise [Errors::UnknownAttribute] if an unknown attribute is encountered and not ignored
101
112
  # @return [void]
102
113
  def assign_attributes(attrs)
103
114
  attrs.each do |k, v|
@@ -119,9 +130,11 @@ module InteractorSupport
119
130
 
120
131
  class << self
121
132
  ##
122
- # Custom constructor that optionally returns the context instead of the object itself.
133
+ # Custom constructor with pluggable return behavior.
123
134
  #
124
- # Behavior is configured via `InteractorSupport.configuration.request_object_behavior`.
135
+ # Controlled by `InteractorSupport.configuration.request_object_behavior`:
136
+ # - `:returns_self` returns the request object instance (good for explicit `#valid?` checks).
137
+ # - `:returns_context` (default) immediately calls {#to_context} for interactor-style hashes/structs.
125
138
  #
126
139
  # @param args [Array] positional args
127
140
  # @param kwargs [Hash] keyword args
@@ -133,8 +146,10 @@ module InteractorSupport
133
146
  end
134
147
 
135
148
  ##
136
- # Defines whether to ignore unknown attributes during assignment.
137
- # If true, unknown attributes are logged but not raised as errors.
149
+ # Declares that unknown attributes should be ignored instead of raising.
150
+ #
151
+ # Ignored keys can still be logged (controlled via
152
+ # `InteractorSupport.configuration.log_unknown_request_object_attributes`).
138
153
  # @example
139
154
  # class MyRequest
140
155
  # include InteractorSupport::RequestObject
@@ -146,17 +161,16 @@ module InteractorSupport
146
161
  end
147
162
 
148
163
  ##
149
- # Defines one or more attributes with optional coercion, default values, transformation,
150
- # and an optional `rewrite:` key to rename the underlying attribute.
164
+ # Declares one or more attributes with optional coercion, defaults, transforms, and key rewrites.
151
165
  #
152
- # @param names [Array<Symbol>] the attribute names
153
- # @param type [Class, nil] optional class to coerce the value to (often another request object)
154
- # @param array [Boolean] whether to treat the input as an array of typed objects
155
- # @param default [Object] default value if not provided
156
- # @param transform [Symbol, Array<Symbol>] method(s) to apply to the value
157
- # @param rewrite [Symbol, nil] optional internal name to rewrite this attribute to
166
+ # @param names [Array<Symbol>] the attribute names declared on the public API
167
+ # @param type [Class, Symbol, nil] optional coercion target (ActiveModel symbol or another request object)
168
+ # @param array [Boolean] treat input as an array of typed objects and coerce each element
169
+ # @param default [Object] default value if not provided by the caller
170
+ # @param transform [Symbol, Array<Symbol>, Proc] one or more transformations applied before coercion
171
+ # @param rewrite [Symbol, nil] internal name to assign stored value to (useful for renaming keys)
158
172
  #
159
- # @raise [ArgumentError] if a transform method is not found
173
+ # @raise [ArgumentError] if a transform method cannot be resolved
160
174
  def attribute(*names, type: nil, array: false, default: nil, transform: nil, rewrite: nil)
161
175
  names.each do |name|
162
176
  attr_name = rewrite || name
@@ -179,7 +193,7 @@ module InteractorSupport
179
193
  end
180
194
  end
181
195
 
182
- # If a `type` is specified, we attempt to cast the `value` to that type
196
+ # If a `type` is specified, cast the value to the configured type before assignment.
183
197
  if type
184
198
  value = array ? Array(value).map { |v| cast_value(v, type) } : cast_value(value, type)
185
199
  end
@@ -2,13 +2,13 @@ require 'active_model'
2
2
 
3
3
  module InteractorSupport
4
4
  ##
5
- # Provides context-aware validation DSL for interactors.
5
+ # Provides a validation DSL tailored to interactor context.
6
6
  #
7
- # This module adds `ActiveModel::Validations` and wraps it with methods like
8
- # `required`, `optional`, `validates_before`, and `validates_after`, allowing
9
- # declarative validation of interactor context values.
10
- #
11
- # Validations are executed automatically before (or after) the interactor runs.
7
+ # Including this module:
8
+ # - Adds `ActiveModel::Validations`
9
+ # - Introduces `required`, `optional`, `validates_before`, and `validates_after` helpers
10
+ # - Automatically maps validated attributes to `context` accessors
11
+ # - Halts execution with `context.fail!` when validations fail
12
12
  #
13
13
  # @example Required attributes with ActiveModel rules
14
14
  # required :email, :name
@@ -37,8 +37,8 @@ module InteractorSupport
37
37
  ##
38
38
  # Declares one or more attributes as required.
39
39
  #
40
- # Values must be present in the context. You can also pass validation options
41
- # as a hash, which will be forwarded to ActiveModel's `validates`.
40
+ # Presence is enforced automatically, and any provided options are forwarded to
41
+ # `ActiveModel::Validations#validates`.
42
42
  #
43
43
  # @param keys [Array<Symbol, Hash>] attribute names or hash of attributes with validation options
44
44
  def required(*keys)
@@ -48,7 +48,7 @@ module InteractorSupport
48
48
  ##
49
49
  # Declares one or more attributes as optional.
50
50
  #
51
- # Optional values can be nil, but still support validation rules.
51
+ # Optional values may be `nil` yet still participate in the provided validations.
52
52
  #
53
53
  # @param keys [Array<Symbol, Hash>] attribute names or hash of attributes with validation options
54
54
  def optional(*keys)
@@ -58,8 +58,7 @@ module InteractorSupport
58
58
  ##
59
59
  # Runs additional validations *after* the interactor executes.
60
60
  #
61
- # Useful for checking persisted records, custom conditions, or results
62
- # that depend on post-processing logic.
61
+ # Use this for checks that depend on side-effects inside `call` (e.g., persistence, background jobs).
63
62
  #
64
63
  # @param keys [Array<Symbol>] context keys to validate
65
64
  # @param validations [Hash] validation options (e.g., presence:, type:, inclusion:, persisted:)
@@ -74,9 +73,8 @@ module InteractorSupport
74
73
  ##
75
74
  # Runs validations *before* the interactor executes.
76
75
  #
77
- # Prevents invalid data from reaching business logic.
78
- #
79
- # NOTE: `persisted:` validation is only available in `validates_after`.
76
+ # Use this to guard business logic from invalid input. The `persisted:` check is only available in
77
+ # {#validates_after} because it depends on side-effects.
80
78
  #
81
79
  # @param keys [Array<Symbol>] context keys to validate
82
80
  # @param validations [Hash] validation options (e.g., presence:, type:, inclusion:)
@@ -95,7 +93,7 @@ module InteractorSupport
95
93
  private
96
94
 
97
95
  ##
98
- # Applies ActiveModel-based validations and wires up accessors to context.
96
+ # Applies ActiveModel validations and wires up reader/writer methods to the context.
99
97
  #
100
98
  # @param keys [Array<Symbol, Hash>] attributes to validate
101
99
  # @param required [Boolean] whether presence is enforced
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module InteractorSupport
4
- VERSION = '1.0.6'
4
+ VERSION = '1.0.7'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: interactor_support
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.6
4
+ version: 1.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Charlie Mitchell