interactor_support 1.0.3 → 1.0.5
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 +12 -0
- data/README.md +179 -0
- data/lib/interactor_support/actions.rb +7 -5
- data/lib/interactor_support/concerns/organizable.rb +194 -0
- data/lib/interactor_support/configuration.rb +22 -0
- data/lib/interactor_support/errors.rb +9 -0
- data/lib/interactor_support/request_object.rb +41 -0
- data/lib/interactor_support/response_object.rb +0 -0
- data/lib/interactor_support/version.rb +1 -1
- data/lib/interactor_support.rb +1 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 315dd265352ed997c5b69934609478e6e5f8b61bbe74bba065acf495f7dac41c
|
|
4
|
+
data.tar.gz: 6a942d0ccb3f3323d156d85f302987b4af7809cdcce852dca56fb9b1ae8419bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2fb25732876e4702b12da283cf4d205504c597ecb904cc9406af3a9825814e8ba518e84bcc615c1c8aadc31ed81cfb9008e2addf759273c2688e029d5b07628e
|
|
7
|
+
data.tar.gz: 5615bf986b8ab272cff775eebbd874c336be93f4e17333d37fd6e87a04b9d74d110b2fcab2a48fb1e0ff86956785b4a7825d03f4201701700566825c2335c633
|
data/CHANGELOG.md
CHANGED
|
@@ -17,3 +17,15 @@
|
|
|
17
17
|
- Added support for rewriting attribute names in a request object
|
|
18
18
|
- Better support for type coersion, using Active model + Array, Hash, and Symbol
|
|
19
19
|
- Better support for `AnyClass` type validations
|
|
20
|
+
|
|
21
|
+
## [1.0.4] - 2025-04-05
|
|
22
|
+
|
|
23
|
+
- Added the organizable concern
|
|
24
|
+
|
|
25
|
+
## [1.0.5] - 2025-06-30
|
|
26
|
+
|
|
27
|
+
- Add support for ignoring unknown attributes in RequestObjects via `ignore_unknown_attributes` class method
|
|
28
|
+
- Introduce `InteractorSupport.configuration.log_unknown_request_object_attributes` to optionally log ignored attributes
|
|
29
|
+
- Introduce `InteractorSupport.configuration.logger` and `log_level` for customizable logging
|
|
30
|
+
- Override `assign_attributes` to integrate attribute ignoring and error-raising behavior
|
|
31
|
+
- Improve test coverage for unknown attribute handling and logging
|
data/README.md
CHANGED
|
@@ -148,6 +148,7 @@ Instead of raw hashes, **Request Objects** provide **validation, transformation,
|
|
|
148
148
|
- Works just like an **ActiveRecord model**
|
|
149
149
|
- Supports **validations** out of the box
|
|
150
150
|
- Automatically **transforms & sanitizes** data
|
|
151
|
+
- Gracefully ignores unknown attributes if configured, with optional logging
|
|
151
152
|
|
|
152
153
|
```ruby
|
|
153
154
|
class TodoRequest
|
|
@@ -162,6 +163,31 @@ class TodoRequest
|
|
|
162
163
|
end
|
|
163
164
|
```
|
|
164
165
|
|
|
166
|
+
### 🔍 Ignoring Unknown Attributes
|
|
167
|
+
|
|
168
|
+
To prevent `RequestObject` from raising an error on unexpected keys, declare `ignore_unknown_attributes` in your class.
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
class CalendarRequest
|
|
172
|
+
include InteractorSupport::RequestObject
|
|
173
|
+
|
|
174
|
+
ignore_unknown_attributes
|
|
175
|
+
|
|
176
|
+
attribute :start_date, type: :date
|
|
177
|
+
attribute :end_date, type: :date
|
|
178
|
+
attribute :timezone, transform: :strip
|
|
179
|
+
|
|
180
|
+
validates :start_date, :end_date, presence: true
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Now, if you initialize the request with an unexpected attribute, it will be logged (if logging is enabled) and ignored:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
CalendarRequest.new(start_date: "2025-07-01", end_date: "2025-07-02", foo: "bar")
|
|
188
|
+
# => No exception raised; 'foo' is ignored.
|
|
189
|
+
```
|
|
190
|
+
|
|
165
191
|
---
|
|
166
192
|
|
|
167
193
|
## 📖 **Documentation**
|
|
@@ -527,6 +553,159 @@ UserRequest.new(params.require(:user).permit(:name, :email, :age))
|
|
|
527
553
|
|
|
528
554
|
But with RequestObject, that’s often unnecessary because you’re already defining a schema.
|
|
529
555
|
|
|
556
|
+
## InteractorSupport::Organizable
|
|
557
|
+
|
|
558
|
+
The Organizable concern provides utility methods to simplify working with interactors and request objects. It gives you a clean and consistent pattern for extracting, transforming, and preparing parameters for use in service objects or interactors.
|
|
559
|
+
|
|
560
|
+
Features
|
|
561
|
+
|
|
562
|
+
- organize: Call interactors with request objects, optionally namespaced under a context_key.
|
|
563
|
+
- request_params: Extract, shape, filter, rename, flatten, and merge incoming params in a clear and declarative way.
|
|
564
|
+
- Built for controllers or service entry points.
|
|
565
|
+
- Rails-native feel — works seamlessly with strong params.
|
|
566
|
+
|
|
567
|
+
#### API Reference
|
|
568
|
+
|
|
569
|
+
**#organize(interactor, params:, request_object:, context_key: nil)**
|
|
570
|
+
Calls the given interactor with a request object built from the provided params.
|
|
571
|
+
|
|
572
|
+
| Argument | Type | Description |
|
|
573
|
+
| -------------- | ------------- | --------------------------------------------------------------------------- |
|
|
574
|
+
| interactor | Class | The interactor to call (.call must be defined). |
|
|
575
|
+
| params | Hash | Parameters passed to the request object. |
|
|
576
|
+
| request_object | Class | A request object class that accepts params in its initializer. |
|
|
577
|
+
| context_key | Symbol or nil | Optional key to namespace the request object inside the interactor context. |
|
|
578
|
+
|
|
579
|
+
Examples
|
|
580
|
+
|
|
581
|
+
```rb
|
|
582
|
+
organize(MyInteractor, params: request_params, request_object: MyRequest)
|
|
583
|
+
|
|
584
|
+
# => MyInteractor.call(MyRequest.new(params))
|
|
585
|
+
|
|
586
|
+
organize(MyInteractor, params: request_params, request_object: MyRequest, context_key: :request)
|
|
587
|
+
|
|
588
|
+
# => MyInteractor.call({ request: MyRequest.new(params) })
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
#### #request_params(\*top_level_keys, merge: {}, except: [], rewrite: [])
|
|
592
|
+
|
|
593
|
+
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.
|
|
594
|
+
|
|
595
|
+
| Argument | Type | Description |
|
|
596
|
+
| ----------------- | -------------------------------- | ------------------------------------------------------------------------- |
|
|
597
|
+
| `*top_level_keys` | `Symbol...` | Optional list of top-level keys to include. If omitted, includes all. |
|
|
598
|
+
| `merge:` | `Hash` | Extra values to merge into the result. |
|
|
599
|
+
| `except:` | `Array<Symbol or Array<Symbol>>` | Keys or nested key paths to exclude. |
|
|
600
|
+
| `rewrite:` | `Array<Hash>` | Rules for renaming, flattening, filtering, merging, or defaulting values. |
|
|
601
|
+
|
|
602
|
+
Rewrite Options
|
|
603
|
+
|
|
604
|
+
Each rewrite entry is a hash in the form { key => options }, where options may include:
|
|
605
|
+
| Option | Type | Description |
|
|
606
|
+
|-----------|---------------------------|-------------------------------------------------------------------|
|
|
607
|
+
| `as` | `Symbol` | Rename the key to a new top-level key. |
|
|
608
|
+
| `only` | `Array<Symbol>` | Include only these subkeys in the result. |
|
|
609
|
+
| `except` | `Array<Symbol>` | Remove these subkeys from the result. |
|
|
610
|
+
| `flatten` | `true` or `Array<Symbol>` | Flatten all subkeys into top-level (or just the specified ones). |
|
|
611
|
+
| `default` | `Hash` | Use this value if the original key is missing or nil. |
|
|
612
|
+
| `merge` | `Hash` | Merge this hash into the result (after filtering and flattening). |
|
|
613
|
+
|
|
614
|
+
Example: full usage
|
|
615
|
+
|
|
616
|
+
```rb
|
|
617
|
+
# Incoming params:
|
|
618
|
+
params = {
|
|
619
|
+
order: {
|
|
620
|
+
product_id: 1,
|
|
621
|
+
quantity: 2,
|
|
622
|
+
internal: "should be removed"
|
|
623
|
+
},
|
|
624
|
+
metadata: {
|
|
625
|
+
source: "mobile",
|
|
626
|
+
internal: "hidden",
|
|
627
|
+
location: { ip: "1.2.3.4" }
|
|
628
|
+
},
|
|
629
|
+
flags: {
|
|
630
|
+
foo: true
|
|
631
|
+
},
|
|
632
|
+
internal: "global_internal",
|
|
633
|
+
session: nil
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
# Incantation:
|
|
637
|
+
request_params(:order, :metadata, :flags, :session,
|
|
638
|
+
merge: { user: current_user }, # <- Add the user
|
|
639
|
+
except: [[:order, :internal], :internal], # <- remove `order.internal`, and the top level key `internal`
|
|
640
|
+
rewrite: [
|
|
641
|
+
{ order: { flatten: true } }, # <- moves all the values from order to top level keys
|
|
642
|
+
{ metadata: { as: :meta, only: [:source, :location], flatten: [:location] } }, # <- Rename metadata to meta, pluck source and location, move location's values to meta
|
|
643
|
+
{ flags: { merge: { debug: true } } }, # <- add flags.debug = true
|
|
644
|
+
{ session: { default: { id: nil } } } # <- create a default value for session
|
|
645
|
+
]
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
# Result
|
|
649
|
+
{
|
|
650
|
+
product_id: 1,
|
|
651
|
+
quantity: 2,
|
|
652
|
+
meta: {
|
|
653
|
+
source: "mobile",
|
|
654
|
+
ip: "1.2.3.4"
|
|
655
|
+
},
|
|
656
|
+
flags: {
|
|
657
|
+
foo: true,
|
|
658
|
+
debug: true
|
|
659
|
+
},
|
|
660
|
+
session: {
|
|
661
|
+
id: nil
|
|
662
|
+
},
|
|
663
|
+
user: current_user
|
|
664
|
+
}
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
⚠️ Array flattening is not supported
|
|
668
|
+
|
|
669
|
+
Flattening arrays of hashes (e.g., { events: [{ id: 1 }] }) is intentionally not supported to avoid accidental key collisions. If needed, transform such structures manually before passing to request_params.
|
|
670
|
+
|
|
671
|
+
**Usage**
|
|
672
|
+
|
|
673
|
+
Include in a controller or service base class
|
|
674
|
+
|
|
675
|
+
```rb
|
|
676
|
+
class ApplicationController < ActionController::Base
|
|
677
|
+
include InteractorSupport::Concerns::Organizable
|
|
678
|
+
end
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
---
|
|
682
|
+
|
|
683
|
+
## 🛠 Configuration Reference
|
|
684
|
+
|
|
685
|
+
All global settings for InteractorSupport can be set via the `InteractorSupport.configure` block. Here's a full list of configuration options:
|
|
686
|
+
|
|
687
|
+
<!-- prettier-ignore-start -->
|
|
688
|
+
| Key | Type | Default | Description |
|
|
689
|
+
| --- | ---- | ------- | -----------|
|
|
690
|
+
| `logger` | `Logger` | `Logger.new($stdout)` | Logger instance used for internal logging. |
|
|
691
|
+
| `log_level` | `Integer` (Logger constant) | `Logger::INFO` | Logging level (e.g., `Logger::DEBUG`, `Logger::WARN`, etc.). |
|
|
692
|
+
| `log_unknown_request_object_attributes` | `Boolean` | `true` | Whether to log unknown request attributes that are ignored. |
|
|
693
|
+
| `request_object_behavior` | `Symbol` | `:returns_context` | Controls what `RequestObject.new(...)` returns (`:returns_self` or `:returns_context`). |
|
|
694
|
+
| `request_object_key_type` | `Symbol` | `:symbol` | Controls the output format of keys in `#to_context` (`:symbol`, `:string`, `:struct`). |
|
|
695
|
+
<!-- prettier-ignore-end -->
|
|
696
|
+
|
|
697
|
+
To update these settings, use:
|
|
698
|
+
|
|
699
|
+
```ruby
|
|
700
|
+
InteractorSupport.configure do |config|
|
|
701
|
+
config.logger = Rails.logger
|
|
702
|
+
config.log_level = Logger::WARN
|
|
703
|
+
config.log_unknown_request_object_attributes = true
|
|
704
|
+
config.request_object_behavior = :returns_self
|
|
705
|
+
config.request_object_key_type = :struct
|
|
706
|
+
end
|
|
707
|
+
```
|
|
708
|
+
|
|
530
709
|
## 🤝 **Contributing**
|
|
531
710
|
|
|
532
711
|
Pull requests are welcome on [GitHub](https://github.com/charliemitchell/interactor_support).
|
|
@@ -7,11 +7,11 @@ module InteractorSupport
|
|
|
7
7
|
# This module is intended to be included into an `Interactor` or `Organizer`,
|
|
8
8
|
# providing access to a suite of declarative action helpers:
|
|
9
9
|
#
|
|
10
|
-
# - {Skippable} — Conditionally skip execution
|
|
11
|
-
# - {Transactionable} — Wrap logic in an ActiveRecord transaction
|
|
12
|
-
# - {Updatable} — Update records using context-driven attributes
|
|
13
|
-
# - {Findable} — Find one or many records into context
|
|
14
|
-
# - {Transformable} — Normalize or modify context values before execution
|
|
10
|
+
# - {InteractorSupport::Concerns::Skippable} — Conditionally skip execution
|
|
11
|
+
# - {InteractorSupport::Concerns::Transactionable} — Wrap logic in an ActiveRecord transaction
|
|
12
|
+
# - {InteractorSupport::Concerns::Updatable} — Update records using context-driven attributes
|
|
13
|
+
# - {InteractorSupport::Concerns::Findable} — Find one or many records into context
|
|
14
|
+
# - {InteractorSupport::Concerns::Transformable} — Normalize or modify context values before execution
|
|
15
15
|
#
|
|
16
16
|
# @example Use in an interactor
|
|
17
17
|
# class UpdateUser
|
|
@@ -25,6 +25,7 @@ module InteractorSupport
|
|
|
25
25
|
# update :user, attributes: { email: :email }
|
|
26
26
|
# end
|
|
27
27
|
#
|
|
28
|
+
#
|
|
28
29
|
# @see InteractorSupport::Concerns::Skippable
|
|
29
30
|
# @see InteractorSupport::Concerns::Transactionable
|
|
30
31
|
# @see InteractorSupport::Concerns::Updatable
|
|
@@ -32,6 +33,7 @@ module InteractorSupport
|
|
|
32
33
|
# @see InteractorSupport::Concerns::Transformable
|
|
33
34
|
module Actions
|
|
34
35
|
extend ActiveSupport::Concern
|
|
36
|
+
|
|
35
37
|
included do
|
|
36
38
|
include InteractorSupport::Concerns::Skippable
|
|
37
39
|
include InteractorSupport::Concerns::Transactionable
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InteractorSupport
|
|
4
|
+
module Concerns
|
|
5
|
+
##
|
|
6
|
+
# The `Organizable` module provides utility methods for organizing interactors
|
|
7
|
+
# and shaping request parameters in a structured way.
|
|
8
|
+
#
|
|
9
|
+
# It is intended to be included into a controller or a base service class that
|
|
10
|
+
# delegates to interactors using request objects.
|
|
11
|
+
#
|
|
12
|
+
# @example Include in a controller
|
|
13
|
+
# class ApplicationController < ActionController::Base
|
|
14
|
+
# include InteractorSupport::Organizable
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# @see InteractorSupport::Organizable#organize
|
|
18
|
+
# @see InteractorSupport::Organizable#request_params
|
|
19
|
+
module Organizable
|
|
20
|
+
include ActiveSupport::Concern
|
|
21
|
+
|
|
22
|
+
# Calls the given interactor with a request object.
|
|
23
|
+
# Optionally wraps the request object under a key in the interactor context.
|
|
24
|
+
#
|
|
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)`.
|
|
28
|
+
# @param context_key [Symbol, nil] Optional key to assign the request object under in the context.
|
|
29
|
+
#
|
|
30
|
+
# @return [void]
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# organize(MyInteractor, params: request_params, request_object: MyRequest)
|
|
34
|
+
# # => Calls MyInteractor with an instance of MyRequest initialized with request_params.
|
|
35
|
+
#
|
|
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
|
+
@context = interactor.call(
|
|
42
|
+
context_key ? { context_key => request_object.new(params) } : request_object.new(params),
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Builds a structured and optionally transformed parameter hash from Rails' `params`.
|
|
47
|
+
#
|
|
48
|
+
# This method supports extracting specific top-level keys, applying optional rewrite
|
|
49
|
+
# transformations, merging in additional values, and excluding unwanted keys.
|
|
50
|
+
#
|
|
51
|
+
# @param top_level_keys [Array<Symbol>] Top-level keys to extract from `params`. If empty, all keys are included.
|
|
52
|
+
# @param merge [Hash] Additional values to merge into the final result.
|
|
53
|
+
# @param except [Array<Symbol, Array<Symbol>>] Keys or nested key paths to exclude from the result.
|
|
54
|
+
# @param rewrite [Array<Hash>] A set of transformation rules applied to the top-level keys.
|
|
55
|
+
#
|
|
56
|
+
# @return [Hash] The final, shaped parameters hash.
|
|
57
|
+
#
|
|
58
|
+
# @example Extracting a specific top-level key
|
|
59
|
+
# # Given: params = { order: { product_id: 1, quantity: 2 } }
|
|
60
|
+
# request_params(:order)
|
|
61
|
+
# # => { order: { product_id: 1, quantity: 2 } }
|
|
62
|
+
#
|
|
63
|
+
# @example Without top-level keys (includes all)
|
|
64
|
+
# # Given: params = { order: { product_id: 1 }, app_id: 123 }
|
|
65
|
+
# request_params()
|
|
66
|
+
# # => { order: { product_id: 1 }, app_id: 123 }
|
|
67
|
+
#
|
|
68
|
+
# @example Merging and excluding
|
|
69
|
+
# # Given: params = { order: { product_id: 1, quantity: 2 }, internal: "yes" }
|
|
70
|
+
# request_params(:order, merge: { user_id: 123 }, except: [[:order, :quantity], :internal])
|
|
71
|
+
# # => { order: { product_id: 1 }, user_id: 123 }
|
|
72
|
+
#
|
|
73
|
+
# @example Flattening a nested hash into the top-level
|
|
74
|
+
# # Given: params = { order: { product_id: 1, quantity: 2 }, app_id: 123 }
|
|
75
|
+
# request_params(:order, rewrite: [{ order: { flatten: true } }])
|
|
76
|
+
# # => { product_id: 1, quantity: 2 }
|
|
77
|
+
#
|
|
78
|
+
# @example Rename a top-level key and filter nested keys
|
|
79
|
+
# # Given: params = { metadata: { source: "mobile", internal: "x" } }
|
|
80
|
+
# request_params(:metadata, rewrite: [
|
|
81
|
+
# { metadata: { as: :meta, only: [:source] } }
|
|
82
|
+
# ])
|
|
83
|
+
# # => { meta: { source: "mobile" } }
|
|
84
|
+
#
|
|
85
|
+
# @example Provide a default value if a key is missing
|
|
86
|
+
# # Given: params = {}
|
|
87
|
+
# request_params(:session, rewrite: [
|
|
88
|
+
# { session: { default: { id: nil } } }
|
|
89
|
+
# ])
|
|
90
|
+
# # => { session: { id: nil } }
|
|
91
|
+
#
|
|
92
|
+
# @example Merge values into a nested structure
|
|
93
|
+
# # Given: params = { flags: { foo: true } }
|
|
94
|
+
# request_params(:flags, rewrite: [
|
|
95
|
+
# { flags: { merge: { debug: true } } }
|
|
96
|
+
# ])
|
|
97
|
+
# # => { flags: { foo: true, debug: true } }
|
|
98
|
+
#
|
|
99
|
+
# @example Combine multiple rewrite rules
|
|
100
|
+
# # Given:
|
|
101
|
+
# # params = {
|
|
102
|
+
# # order: { product_id: 1, quantity: 2 },
|
|
103
|
+
# # metadata: { source: "mobile", location: { ip: "1.2.3.4" } },
|
|
104
|
+
# # tracking: { click_id: "abc", session_id: "def" }
|
|
105
|
+
# # }
|
|
106
|
+
# request_params(:order, :metadata, :tracking, rewrite: [
|
|
107
|
+
# { order: { flatten: true } },
|
|
108
|
+
# { metadata: { as: :meta, only: [:source, :location], flatten: [:location] } }
|
|
109
|
+
# ])
|
|
110
|
+
# # => {
|
|
111
|
+
# # product_id: 1,
|
|
112
|
+
# # quantity: 2,
|
|
113
|
+
# # meta: { source: "mobile", ip: "1.2.3.4" },
|
|
114
|
+
# # tracking: { click_id: "abc", session_id: "def" }
|
|
115
|
+
# # }
|
|
116
|
+
def request_params(*top_level_keys, merge: {}, except: [], rewrite: [])
|
|
117
|
+
permitted = params.permit!.to_h.deep_symbolize_keys
|
|
118
|
+
data = top_level_keys.any? ? permitted.slice(*top_level_keys) : permitted
|
|
119
|
+
|
|
120
|
+
apply_rewrites!(data, rewrite)
|
|
121
|
+
|
|
122
|
+
data
|
|
123
|
+
.deep_merge(merge)
|
|
124
|
+
.then { |result| except.any? ? deep_except(result, except) : result }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def apply_rewrites!(data, rewrites)
|
|
130
|
+
rewrites.each do |rule|
|
|
131
|
+
key, config = rule.first
|
|
132
|
+
config = { flatten: true } if config == :flatten
|
|
133
|
+
|
|
134
|
+
original = data.key?(key) ? data.delete(key) : nil
|
|
135
|
+
transformed = original.deep_dup if original.is_a?(Hash)
|
|
136
|
+
transformed ||= original
|
|
137
|
+
|
|
138
|
+
# Filtering
|
|
139
|
+
transformed.slice!(*config[:only]) if config[:only] && transformed.respond_to?(:slice!)
|
|
140
|
+
transformed.except!(*config[:except]) if config[:except] && transformed.respond_to?(:except!)
|
|
141
|
+
|
|
142
|
+
# Flatten specific nested keys
|
|
143
|
+
if config[:flatten].is_a?(Array) && transformed.is_a?(Hash)
|
|
144
|
+
config[:flatten].each do |subkey|
|
|
145
|
+
nested = transformed.delete(subkey)
|
|
146
|
+
if nested.is_a?(Hash)
|
|
147
|
+
transformed.merge!(nested)
|
|
148
|
+
elsif nested.is_a?(Array)
|
|
149
|
+
raise ArgumentError,
|
|
150
|
+
"Cannot flatten array for the key `#{subkey}`. Flattening arrays of hashes is not supported."
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Apply default if nil or missing
|
|
156
|
+
transformed ||= config[:default]
|
|
157
|
+
|
|
158
|
+
# Merge additional keys
|
|
159
|
+
if config[:merge]
|
|
160
|
+
transformed = transformed.is_a?(Hash) ? transformed.merge(config[:merge]) : config[:merge]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Fully flatten to top level
|
|
164
|
+
if config[:flatten] == true && transformed.is_a?(Hash)
|
|
165
|
+
data.merge!(transformed)
|
|
166
|
+
else
|
|
167
|
+
target_key = config[:as] || key
|
|
168
|
+
data[target_key] = transformed
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def deep_except(hash, paths)
|
|
174
|
+
paths.reduce(hash) { |acc, path| remove_nested_key(acc, Array(path)) }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def remove_nested_key(hash, path)
|
|
178
|
+
return hash unless path.is_a?(Array) && path.any?
|
|
179
|
+
|
|
180
|
+
key, *rest = path
|
|
181
|
+
return hash unless hash.key?(key)
|
|
182
|
+
|
|
183
|
+
duped = hash.dup
|
|
184
|
+
if rest.empty?
|
|
185
|
+
duped.delete(key)
|
|
186
|
+
elsif duped[key].is_a?(Hash)
|
|
187
|
+
duped[key] = remove_nested_key(duped[key], rest)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
duped
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -29,13 +29,35 @@ module InteractorSupport
|
|
|
29
29
|
# @return [:string, :symbol, :struct]
|
|
30
30
|
attr_accessor :request_object_key_type
|
|
31
31
|
|
|
32
|
+
##
|
|
33
|
+
# Logger for InteractorSupport, defaults to STDOUT.
|
|
34
|
+
# @return [Logger]
|
|
35
|
+
attr_accessor :logger
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# The log level for InteractorSupport logs.
|
|
39
|
+
# @return [Integer]
|
|
40
|
+
attr_accessor :log_level
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Whether to log unknown request object attributes when they are ignored.
|
|
44
|
+
# If true, logs a warning when an unknown attribute is encountered.
|
|
45
|
+
# @see InteractorSupport::RequestObject#ignore_unknown_attributes
|
|
46
|
+
attr_accessor :log_unknown_request_object_attributes
|
|
47
|
+
|
|
32
48
|
##
|
|
33
49
|
# Initializes the configuration with default values:
|
|
34
50
|
# - `request_object_behavior` defaults to `:returns_context`
|
|
35
51
|
# - `request_object_key_type` defaults to `:symbol`
|
|
52
|
+
# - `logger` defaults to a new Logger instance writing to STDOUT
|
|
53
|
+
# - `log_level` defaults to `Logger::INFO`
|
|
54
|
+
# - `log_unknown_request_object_attributes` defaults to `true`
|
|
36
55
|
def initialize
|
|
37
56
|
@request_object_behavior = :returns_context
|
|
38
57
|
@request_object_key_type = :symbol
|
|
58
|
+
@logger = Logger.new($stdout)
|
|
59
|
+
@log_level = Logger::INFO
|
|
60
|
+
@log_unknown_request_object_attributes = true
|
|
39
61
|
end
|
|
40
62
|
end
|
|
41
63
|
end
|
|
@@ -40,6 +40,7 @@ module InteractorSupport
|
|
|
40
40
|
included do
|
|
41
41
|
include ActiveModel::Model
|
|
42
42
|
include ActiveModel::Attributes
|
|
43
|
+
include ActiveModel::AttributeAssignment
|
|
43
44
|
include ActiveModel::Validations::Callbacks
|
|
44
45
|
|
|
45
46
|
##
|
|
@@ -82,11 +83,38 @@ module InteractorSupport
|
|
|
82
83
|
value
|
|
83
84
|
end
|
|
84
85
|
end
|
|
86
|
+
|
|
85
87
|
return Struct.new(*attrs.keys).new(*attrs.values) if key_type == :struct
|
|
86
88
|
|
|
87
89
|
attrs
|
|
88
90
|
end
|
|
89
91
|
|
|
92
|
+
##
|
|
93
|
+
# Assigns the given attributes to the request object.
|
|
94
|
+
#
|
|
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`.
|
|
98
|
+
#
|
|
99
|
+
# @param attrs [Hash] input attributes to assign
|
|
100
|
+
# @raise [Errors::UnknownAttribute] if unknown attribute is encountered and not ignored
|
|
101
|
+
# @return [void]
|
|
102
|
+
def assign_attributes(attrs)
|
|
103
|
+
attrs.each do |k, v|
|
|
104
|
+
setter = "#{k}="
|
|
105
|
+
if respond_to?(setter)
|
|
106
|
+
send(setter, v)
|
|
107
|
+
elsif respond_to?(:ignore_unknown_attributes?) && ignore_unknown_attributes?
|
|
108
|
+
InteractorSupport.configuration.logger.log(
|
|
109
|
+
InteractorSupport.configuration.log_level,
|
|
110
|
+
"InteractorSupport::RequestObject ignoring unknown attribute '#{k}' for #{self.class.name}.",
|
|
111
|
+
)
|
|
112
|
+
else
|
|
113
|
+
raise Errors::UnknownAttribute, "`#{k}` for #{self.class.name}."
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
90
118
|
class << self
|
|
91
119
|
##
|
|
92
120
|
# Custom constructor that optionally returns the context instead of the object itself.
|
|
@@ -102,6 +130,19 @@ module InteractorSupport
|
|
|
102
130
|
super(*args, **kwargs).to_context
|
|
103
131
|
end
|
|
104
132
|
|
|
133
|
+
##
|
|
134
|
+
# Defines whether to ignore unknown attributes during assignment.
|
|
135
|
+
# If true, unknown attributes are logged but not raised as errors.
|
|
136
|
+
# @example
|
|
137
|
+
# class MyRequest
|
|
138
|
+
# include InteractorSupport::RequestObject
|
|
139
|
+
# ignore_unknown_attributes
|
|
140
|
+
# end
|
|
141
|
+
# @return [void]
|
|
142
|
+
def ignore_unknown_attributes
|
|
143
|
+
define_method(:ignore_unknown_attributes?) { true }
|
|
144
|
+
end
|
|
145
|
+
|
|
105
146
|
##
|
|
106
147
|
# Defines one or more attributes with optional coercion, default values, transformation,
|
|
107
148
|
# and an optional `rewrite:` key to rename the underlying attribute.
|
|
File without changes
|
data/lib/interactor_support.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: interactor_support
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Charlie Mitchell
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-07-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description:
|
|
14
14
|
email:
|
|
@@ -30,13 +30,16 @@ files:
|
|
|
30
30
|
- lib/interactor_support.rb
|
|
31
31
|
- lib/interactor_support/actions.rb
|
|
32
32
|
- lib/interactor_support/concerns/findable.rb
|
|
33
|
+
- lib/interactor_support/concerns/organizable.rb
|
|
33
34
|
- lib/interactor_support/concerns/skippable.rb
|
|
34
35
|
- lib/interactor_support/concerns/transactionable.rb
|
|
35
36
|
- lib/interactor_support/concerns/transformable.rb
|
|
36
37
|
- lib/interactor_support/concerns/updatable.rb
|
|
37
38
|
- lib/interactor_support/configuration.rb
|
|
38
39
|
- lib/interactor_support/core.rb
|
|
40
|
+
- lib/interactor_support/errors.rb
|
|
39
41
|
- lib/interactor_support/request_object.rb
|
|
42
|
+
- lib/interactor_support/response_object.rb
|
|
40
43
|
- lib/interactor_support/rubocop.rb
|
|
41
44
|
- lib/interactor_support/rubocop/cop/base_interactor_cop.rb
|
|
42
45
|
- lib/interactor_support/rubocop/cop/require_required_for_interactor_support.rb
|