interactor_support 1.0.4 → 1.0.6

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: 4d6d8c9f46a3e48520652cdb83a9257d487a3411feef0b1efd69c1fc4a209c03
4
- data.tar.gz: 31b939d573a1dc042b671304ec777bf4a8bf53ad61bd3b4d63fccc3fd4dd2806
3
+ metadata.gz: bf8cda13999c9971a1499398e23135ef6e870af7581ffe7e9f173b9d7c4231ea
4
+ data.tar.gz: 48c401b5b42e80ccea3db170ecb1e8476861522804b800ad28651eebfa674222
5
5
  SHA512:
6
- metadata.gz: 2cf4e789be122df812109df549d3d94a4cf685af0a85eb3fa3d47e811fc73d2231a4fd5af95a2e3c397505102c363bb831df716f16fe701fa0e2fd33e586e757
7
- data.tar.gz: 8d6e5ee59b6c0523d0d7a9931d6b0ee4f1f98c7aa7d87aabfd45f8dcad8b9f7831444030803abc98bf0594b87a9e084cc9285e8f7382dd378f0d68f4fdc79823
6
+ metadata.gz: 2adf5f317e6432571a24358e9e1ba453397fcfd5f6b83758930fd45bef652dfb01ead87aa68fcfec385151bd60646127a087968e332baa0193e5bb9f7ac14ff6
7
+ data.tar.gz: a6a670cacb4abd50812a82232dcbeb1e582eea72d3526b2659f4dccd827226c6e07c6b12f6c39b282eb276db482d02911cd4d0e125094f8c1d114fba5459c899
data/CHANGELOG.md CHANGED
@@ -21,3 +21,17 @@
21
21
  ## [1.0.4] - 2025-04-05
22
22
 
23
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
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
@@ -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**
@@ -533,7 +559,7 @@ The Organizable concern provides utility methods to simplify working with intera
533
559
 
534
560
  Features
535
561
 
536
- - organize: Call interactors with request objects, optionally namespaced under a context_key.
562
+ - organize: Call interactors with request objects, optionally namespaced under a context_key, and wrap validation failures in a consistent error.
537
563
  - request_params: Extract, shape, filter, rename, flatten, and merge incoming params in a clear and declarative way.
538
564
  - Built for controllers or service entry points.
539
565
  - Rails-native feel — works seamlessly with strong params.
@@ -562,6 +588,18 @@ organize(MyInteractor, params: request_params, request_object: MyRequest, contex
562
588
  # => MyInteractor.call({ request: MyRequest.new(params) })
563
589
  ```
564
590
 
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.
592
+
593
+ ```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
600
+ end
601
+ ```
602
+
565
603
  #### #request_params(\*top_level_keys, merge: {}, except: [], rewrite: [])
566
604
 
567
605
  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.
@@ -652,6 +690,34 @@ class ApplicationController < ActionController::Base
652
690
  end
653
691
  ```
654
692
 
693
+ ---
694
+
695
+ ## 🛠 Configuration Reference
696
+
697
+ All global settings for InteractorSupport can be set via the `InteractorSupport.configure` block. Here's a full list of configuration options:
698
+
699
+ <!-- prettier-ignore-start -->
700
+ | Key | Type | Default | Description |
701
+ | --- | ---- | ------- | -----------|
702
+ | `logger` | `Logger` | `Logger.new($stdout)` | Logger instance used for internal logging. |
703
+ | `log_level` | `Integer` (Logger constant) | `Logger::INFO` | Logging level (e.g., `Logger::DEBUG`, `Logger::WARN`, etc.). |
704
+ | `log_unknown_request_object_attributes` | `Boolean` | `true` | Whether to log unknown request attributes that are ignored. |
705
+ | `request_object_behavior` | `Symbol` | `:returns_context` | Controls what `RequestObject.new(...)` returns (`:returns_self` or `:returns_context`). |
706
+ | `request_object_key_type` | `Symbol` | `:symbol` | Controls the output format of keys in `#to_context` (`:symbol`, `:string`, `:struct`). |
707
+ <!-- prettier-ignore-end -->
708
+
709
+ To update these settings, use:
710
+
711
+ ```ruby
712
+ InteractorSupport.configure do |config|
713
+ config.logger = Rails.logger
714
+ config.log_level = Logger::WARN
715
+ config.log_unknown_request_object_attributes = true
716
+ config.request_object_behavior = :returns_self
717
+ config.request_object_key_type = :struct
718
+ end
719
+ ```
720
+
655
721
  ## 🤝 **Contributing**
656
722
 
657
723
  Pull requests are welcome on [GitHub](https://github.com/charliemitchell/interactor_support).
@@ -38,8 +38,22 @@ module InteractorSupport
38
38
  # # => Calls MyInteractor with an instance of MyRequest initialized with request_params at :context_key.
39
39
  # # # => The context will contain { request: MyRequest.new(request_params) }
40
40
  def organize(interactor, params:, request_object:, context_key: nil)
41
+ request_payload = request_object.new(params)
42
+
41
43
  @context = interactor.call(
42
- context_key ? { context_key => request_object.new(params) } : request_object.new(params),
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
53
+
54
+ raise InteractorSupport::Errors::InvalidRequestObject.new(
55
+ request_class: request_object,
56
+ errors: errors,
43
57
  )
44
58
  end
45
59
 
@@ -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
@@ -0,0 +1,56 @@
1
+ module InteractorSupport
2
+ module Errors
3
+ class UnknownAttribute < StandardError
4
+ attr_reader :attribute, :owner
5
+
6
+ def initialize(attribute, owner: nil)
7
+ @attribute = attribute
8
+ @owner = owner
9
+ super(build_message(attribute, owner))
10
+ end
11
+
12
+ private
13
+
14
+ def build_message(attribute, owner)
15
+ name =
16
+ case attribute
17
+ when String then attribute
18
+ when Symbol then attribute.to_s
19
+ else attribute.inspect
20
+ end
21
+
22
+ owner_name =
23
+ if owner.respond_to?(:name) && owner.name
24
+ owner.name
25
+ elsif owner.respond_to?(:to_s)
26
+ owner.to_s
27
+ else
28
+ owner
29
+ end
30
+
31
+ suffix = owner_name ? " for #{owner_name}" : ''
32
+ "Unknown attribute: #{name}#{suffix}"
33
+ end
34
+ end
35
+
36
+ class InvalidRequestObject < StandardError
37
+ attr_reader :request_class, :errors
38
+
39
+ def initialize(request_class:, errors: [])
40
+ @request_class = request_class
41
+ @errors = Array(errors)
42
+
43
+ request_name =
44
+ if request_class.respond_to?(:name) && request_class.name
45
+ request_class.name
46
+ else
47
+ request_class.to_s
48
+ end
49
+
50
+ detail = @errors.any? ? ": #{@errors.join(', ')}" : ''
51
+
52
+ super("Invalid #{request_name}#{detail}")
53
+ end
54
+ end
55
+ end
56
+ 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
  ##
@@ -88,6 +89,34 @@ module InteractorSupport
88
89
  attrs
89
90
  end
90
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
+ if InteractorSupport.configuration.log_unknown_request_object_attributes
109
+ InteractorSupport.configuration.logger.log(
110
+ InteractorSupport.configuration.log_level,
111
+ "InteractorSupport::RequestObject ignoring unknown attribute '#{k}' for #{self.class.name}.",
112
+ )
113
+ end
114
+ else
115
+ raise Errors::UnknownAttribute.new(k, owner: self.class)
116
+ end
117
+ end
118
+ end
119
+
91
120
  class << self
92
121
  ##
93
122
  # Custom constructor that optionally returns the context instead of the object itself.
@@ -103,6 +132,19 @@ module InteractorSupport
103
132
  super(*args, **kwargs).to_context
104
133
  end
105
134
 
135
+ ##
136
+ # Defines whether to ignore unknown attributes during assignment.
137
+ # If true, unknown attributes are logged but not raised as errors.
138
+ # @example
139
+ # class MyRequest
140
+ # include InteractorSupport::RequestObject
141
+ # ignore_unknown_attributes
142
+ # end
143
+ # @return [void]
144
+ def ignore_unknown_attributes
145
+ define_method(:ignore_unknown_attributes?) { true }
146
+ end
147
+
106
148
  ##
107
149
  # Defines one or more attributes with optional coercion, default values, transformation,
108
150
  # and an optional `rewrite:` key to rename the underlying attribute.
@@ -192,7 +234,13 @@ module InteractorSupport
192
234
  message = ":#{type} is not a supported type. Supported types are: #{SUPPORTED_TYPES.join(", ")}"
193
235
  raise TypeError, message
194
236
  rescue
195
- raise TypeError, "Cannot cast #{value.inspect} to #{type.name}"
237
+ type_name =
238
+ if type.respond_to?(:name) && type.name
239
+ type.name
240
+ else
241
+ type.to_s
242
+ end
243
+ raise TypeError, "Cannot cast #{value.inspect} to #{type_name}"
196
244
  end
197
245
  end
198
246
  end
File without changes
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module InteractorSupport
4
- VERSION = '1.0.4'
4
+ VERSION = '1.0.6'
5
5
  end
@@ -3,6 +3,7 @@
3
3
  require 'interactor'
4
4
  require 'logger'
5
5
  require 'active_support/concern'
6
+ require 'interactor_support/errors'
6
7
  require_relative 'interactor_support/core'
7
8
  require_relative 'interactor_support/version'
8
9
  require_relative 'interactor_support/actions'
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
4
+ version: 1.0.6
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-04-05 00:00:00.000000000 Z
11
+ date: 2025-09-24 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -37,7 +37,9 @@ files:
37
37
  - lib/interactor_support/concerns/updatable.rb
38
38
  - lib/interactor_support/configuration.rb
39
39
  - lib/interactor_support/core.rb
40
+ - lib/interactor_support/errors.rb
40
41
  - lib/interactor_support/request_object.rb
42
+ - lib/interactor_support/response_object.rb
41
43
  - lib/interactor_support/rubocop.rb
42
44
  - lib/interactor_support/rubocop/cop/base_interactor_cop.rb
43
45
  - lib/interactor_support/rubocop/cop/require_required_for_interactor_support.rb