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 +4 -4
- data/CHANGELOG.md +13 -6
- data/README.md +36 -7
- data/Rakefile +11 -11
- data/lib/interactor_support/actions.rb +2 -4
- data/lib/interactor_support/concerns/findable.rb +10 -10
- data/lib/interactor_support/concerns/organizable.rb +361 -38
- data/lib/interactor_support/concerns/skippable.rb +6 -10
- data/lib/interactor_support/concerns/transactionable.rb +8 -10
- data/lib/interactor_support/concerns/transformable.rb +13 -11
- data/lib/interactor_support/concerns/updatable.rb +18 -12
- data/lib/interactor_support/configuration.rb +8 -2
- data/lib/interactor_support/core.rb +3 -4
- data/lib/interactor_support/errors.rb +3 -1
- data/lib/interactor_support/request_object.rb +44 -30
- data/lib/interactor_support/validations.rb +13 -15
- data/lib/interactor_support/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: 2a9492a85b640343b10a985ca038fb2a2bbcd98a5532ae61f0b98abdb41106e9
|
|
4
|
+
data.tar.gz: 29bfa393df505cb275b0deea347b39c012e3e3af2f6f01610f99c1b639c3e0f1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
|
5
|
-
require
|
|
6
|
-
require
|
|
7
|
-
require
|
|
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:
|
|
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
|
|
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
|
|
34
|
+
desc 'Create the database'
|
|
35
35
|
task :create do
|
|
36
36
|
ActiveRecord::Tasks::DatabaseTasks.create_current
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
desc
|
|
39
|
+
desc 'Drop the database'
|
|
40
40
|
task :drop do
|
|
41
41
|
ActiveRecord::Tasks::DatabaseTasks.drop_current
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
desc
|
|
45
|
-
task :
|
|
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
|
-
#
|
|
5
|
-
# composable behavior.
|
|
4
|
+
# Bundles the most common InteractorSupport concerns into a single include.
|
|
6
5
|
#
|
|
7
|
-
#
|
|
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
|
-
#
|
|
4
|
+
# DSL helpers for loading records into context before an interactor runs.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
33
|
-
#
|
|
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
|
-
#
|
|
75
|
-
#
|
|
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
|
-
#
|
|
7
|
-
# and shaping request parameters in a structured way.
|
|
6
|
+
# Utilities for invoking interactors with request objects and shaping incoming params.
|
|
8
7
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
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
|
-
|
|
23
|
-
|
|
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]
|
|
27
|
-
# @param request_object [Class] A request object class that responds to
|
|
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 [
|
|
153
|
+
# @return [Interactor::Context]
|
|
31
154
|
#
|
|
32
|
-
# @example
|
|
33
|
-
#
|
|
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(
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
208
|
+
# Builds a structured parameter hash from Rails' `params`, with helpers for rewriting keys.
|
|
61
209
|
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
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
|
|
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
|
|
6
|
+
# Adds a declarative `skip` helper for short-circuiting interactor execution.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
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
|
|
13
|
-
# - Lambdas
|
|
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
|
-
#
|
|
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
|
|
4
|
+
# Adds a declarative `transaction` wrapper around interactor execution using ActiveRecord.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
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
|
-
#
|
|
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]
|
|
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
|
|
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
|
|
4
|
+
# Adds helpers for preparing context data before the interactor `call` executes.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
|
31
|
-
#
|
|
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}]
|
|
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
|
|
51
|
+
# Transforms one or more context values using symbols, procs, or chains of both.
|
|
52
52
|
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
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
|
-
#
|
|
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]
|
|
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
|
|
4
|
+
# Adds an `update` DSL for synchronizing context data back into ActiveRecord models.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
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
|
-
#
|
|
40
|
+
# Missing data triggers `context.fail!` with a helpful message so the update halts cleanly.
|
|
35
41
|
#
|
|
36
|
-
# @param model [Symbol]
|
|
37
|
-
# @param attributes [Hash, Symbol]
|
|
38
|
-
# @param context_key [Symbol, nil] key to
|
|
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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
3
|
+
# Provides a concise DSL for building validated, transformable, and nested request objects.
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
|
56
|
+
# Initializes the request object, applying key rewrites and validations.
|
|
48
57
|
#
|
|
49
|
-
#
|
|
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
|
|
77
|
+
# Converts the request object into the structure expected by interactors.
|
|
67
78
|
#
|
|
68
|
-
# - If `
|
|
69
|
-
# - If `
|
|
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
|
|
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
|
|
104
|
+
# Assigns external attributes, respecting rewrite rules and the unknown-attribute policy.
|
|
94
105
|
#
|
|
95
|
-
# - Known attributes are
|
|
96
|
-
# - If `ignore_unknown_attributes
|
|
97
|
-
# - Otherwise
|
|
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
|
|
133
|
+
# Custom constructor with pluggable return behavior.
|
|
123
134
|
#
|
|
124
|
-
#
|
|
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
|
-
#
|
|
137
|
-
#
|
|
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
|
-
#
|
|
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
|
|
154
|
-
# @param array [Boolean]
|
|
155
|
-
# @param default [Object] default value if not provided
|
|
156
|
-
# @param transform [Symbol, Array<Symbol
|
|
157
|
-
# @param rewrite [Symbol, nil]
|
|
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
|
|
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,
|
|
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
|
|
5
|
+
# Provides a validation DSL tailored to interactor context.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
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
|
-
#
|
|
41
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|