interactor_support 1.0.2 → 1.0.4

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.
@@ -1,6 +1,39 @@
1
+ # lib/interactor_support/version.rb
1
2
  module InteractorSupport
3
+ ##
4
+ # A bundle of DSL-style concerns that enhance interactors with expressive,
5
+ # composable behavior.
6
+ #
7
+ # This module is intended to be included into an `Interactor` or `Organizer`,
8
+ # providing access to a suite of declarative action helpers:
9
+ #
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
+ #
16
+ # @example Use in an interactor
17
+ # class UpdateUser
18
+ # include Interactor
19
+ # include InteractorSupport::Actions
20
+ #
21
+ # find_by :user
22
+ #
23
+ # transform :email, with: [:strip, :downcase]
24
+ #
25
+ # update :user, attributes: { email: :email }
26
+ # end
27
+ #
28
+ #
29
+ # @see InteractorSupport::Concerns::Skippable
30
+ # @see InteractorSupport::Concerns::Transactionable
31
+ # @see InteractorSupport::Concerns::Updatable
32
+ # @see InteractorSupport::Concerns::Findable
33
+ # @see InteractorSupport::Concerns::Transformable
2
34
  module Actions
3
35
  extend ActiveSupport::Concern
36
+
4
37
  included do
5
38
  include InteractorSupport::Concerns::Skippable
6
39
  include InteractorSupport::Concerns::Transactionable
@@ -1,18 +1,50 @@
1
1
  module InteractorSupport
2
2
  module Concerns
3
+ ##
4
+ # Adds dynamic model-finding helpers (`find_by`, `find_where`) to an interactor.
5
+ #
6
+ # This concern wraps ActiveRecord `.find_by` and `.where` queries into
7
+ # declarative DSL methods that load records into the interactor context.
8
+ #
9
+ # These methods support symbols (for context keys) and lambdas (for dynamic runtime evaluation).
10
+ #
11
+ # @example Find a post by ID from the context
12
+ # find_by :post
13
+ #
14
+ # @example Find by query using context value
15
+ # find_by :post, query: { slug: :slug }, required: true
16
+ #
17
+ # @example Find using a dynamic lambda
18
+ # find_by :post, query: { created_at: -> { 1.week.ago..Time.current } }
19
+ #
20
+ # @example Find all posts for a user with a scope
21
+ # find_where :post, where: { user_id: :user_id }, scope: :published
22
+ #
23
+ # @see InteractorSupport::Actions
3
24
  module Findable
4
25
  extend ActiveSupport::Concern
5
26
  include InteractorSupport::Core
6
-
27
+
7
28
  included do
8
29
  class << self
9
- # This method finds a single record using query parameters.
10
- # Supports symbols (context keys) and lambdas for dynamic values.
30
+ # Adds a `before` callback to find a single record and assign it to context.
31
+ #
32
+ # This method searches for a record based on the provided query parameters.
33
+ # It supports dynamic values using symbols (context keys) and lambdas (for runtime evaluation).
34
+ #
35
+ # @param model [Symbol, String] the name of the model to query (e.g., `:post`)
36
+ # @param query [Hash{Symbol=>Object,Proc}] a hash of attributes to match (can use symbols for context lookup or lambdas)
37
+ # @param context_key [Symbol, nil] the key under which to store the result in context (defaults to the model name)
38
+ # @param required [Boolean] if true, fails the context if no record is found
11
39
  #
12
- # Examples:
13
- # find_by :post, query: { slug: :slug }, context_key: :current_post
14
- # find_by :post, query: { id: :post_id, published: true }
15
- # find_by :post, query: { created_at: -> { 1.week.ago..Time.current } }
40
+ # @example Basic ID-based lookup
41
+ # find_by :post
42
+ #
43
+ # @example Query with context value
44
+ # find_by :post, query: { slug: :slug }
45
+ #
46
+ # @example Query with a lambda
47
+ # find_by :post, query: { created_at: -> { 1.week.ago..Time.current } }
16
48
  def find_by(model, query: {}, context_key: nil, required: false)
17
49
  context_key ||= model
18
50
  before do
@@ -24,9 +56,9 @@ module InteractorSupport
24
56
  model.to_s.classify.constantize.find_by(
25
57
  query.transform_values do |v|
26
58
  case v
27
- when Symbol then context[v] # Lookup symbol in context
28
- when Proc then instance_exec(&v) # Evaluate lambda dynamically
29
- else v # Use raw value
59
+ when Symbol then context[v]
60
+ when Proc then instance_exec(&v)
61
+ else v
30
62
  end
31
63
  end,
32
64
  )
@@ -37,14 +69,26 @@ module InteractorSupport
37
69
  end
38
70
  end
39
71
 
40
- # This method finds multiple records using where conditions.
41
- # Supports symbols (context keys) and lambdas for dynamic values.
72
+ # Adds a `before` callback to find multiple records and assign them to context.
73
+ #
74
+ # This method performs a `.where` query with optional `.where.not` and `.scope`,
75
+ # allowing dynamic values using symbols (for context lookup) and lambdas (for runtime evaluation).
42
76
  #
43
- # Examples:
44
- # find_where :post, where: { user_id: :user_id }
45
- # find_where :post, where: { created_at: -> { 5.days.ago..Time.current } }
46
- # find_where :post, where: { user_id: :user_id }, where_not: { active: false }
47
- # find_where :post, where: { user_id: :user_id }, scope: :active
77
+ # @param model [Symbol, String] the name of the model to query (e.g., `:post`)
78
+ # @param where [Hash{Symbol=>Object,Proc}] conditions for `.where` (can use symbols or lambdas)
79
+ # @param where_not [Hash{Symbol=>Object,Proc}] conditions for `.where.not`
80
+ # @param scope [Symbol, nil] optional named scope to call on the model
81
+ # @param context_key [Symbol, nil] the key under which to store the result in context (defaults to pluralized model name)
82
+ # @param required [Boolean] if true, fails the context if no records are found
83
+ #
84
+ # @example Where query with symbol context values
85
+ # find_where :post, where: { user_id: :user_id }
86
+ #
87
+ # @example Where with a lambda and scope
88
+ # find_where :post, where: { created_at: -> { 5.days.ago..Time.current } }, scope: :published
89
+ #
90
+ # @example Where with exclusions
91
+ # find_where :post, where: { user_id: :user_id }, where_not: { active: false }
48
92
  def find_where(model, where: {}, where_not: {}, scope: nil, context_key: nil, required: false)
49
93
  context_key ||= model.to_s.pluralize.to_sym
50
94
  before do
@@ -53,9 +97,9 @@ module InteractorSupport
53
97
  query = query.where(
54
98
  where.transform_values do |v|
55
99
  case v
56
- when Symbol then context[v] # Lookup symbol in context
57
- when Proc then instance_exec(&v) # Evaluate lambda dynamically
58
- else v # Use raw value
100
+ when Symbol then context[v]
101
+ when Proc then instance_exec(&v)
102
+ else v
59
103
  end
60
104
  end,
61
105
  ) if where.present?
@@ -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
@@ -1,11 +1,53 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module InteractorSupport
2
4
  module Concerns
5
+ ##
6
+ # Adds a DSL method to conditionally skip an interactor.
7
+ #
8
+ # This concern provides a `skip` method that wraps the interactor in an `around` block.
9
+ # You can pass an `:if` or `:unless` condition using a Proc, Symbol, or literal.
10
+ # The condition will be evaluated at runtime to determine whether to run the interactor.
11
+ #
12
+ # - Symbols will be looked up on the interactor or in the context.
13
+ # - Lambdas/Procs are evaluated using `instance_exec` with full access to context.
14
+ #
15
+ # @example Skip if the user is already authenticated (symbol in context)
16
+ # skip if: :user_authenticated
17
+ #
18
+ # @example Skip unless a method returns true
19
+ # skip unless: :should_run?
20
+ #
21
+ # @example Skip based on a lambda
22
+ # skip if: -> { mode == "test" }
23
+ #
24
+ # @see InteractorSupport::Actions
3
25
  module Skippable
4
26
  extend ActiveSupport::Concern
5
27
  include InteractorSupport::Core
6
28
 
7
29
  included do
8
30
  class << self
31
+ ##
32
+ # Skips the interactor based on a condition provided via `:if` or `:unless`.
33
+ #
34
+ # This wraps the interactor in an `around` hook, and conditionally skips
35
+ # execution based on truthy/falsy evaluation of the provided options.
36
+ #
37
+ # The condition can be a Proc (evaluated in context), a Symbol (used to call a method or context key), or a literal value.
38
+ #
39
+ # @param options [Hash]
40
+ # @option options [Proc, Symbol, Boolean] :if a condition that must be truthy to skip
41
+ # @option options [Proc, Symbol, Boolean] :unless a condition that must be falsy to skip
42
+ #
43
+ # @example Skip if a context value is truthy
44
+ # skip if: :user_authenticated
45
+ #
46
+ # @example Skip unless a method returns true
47
+ # skip unless: :should_run?
48
+ #
49
+ # @example Skip based on a lambda
50
+ # skip if: -> { context[:mode] == "test" }
9
51
  def skip(**options)
10
52
  around do |interactor|
11
53
  unless options[:if].nil?
@@ -1,11 +1,48 @@
1
1
  module InteractorSupport
2
2
  module Concerns
3
+ ##
4
+ # Adds transactional support to your interactor using ActiveRecord.
5
+ #
6
+ # The `transaction` method wraps the interactor execution in an `around` block
7
+ # that uses `ActiveRecord::Base.transaction`. If the context fails (via `context.fail!`),
8
+ # the transaction is rolled back automatically using `ActiveRecord::Rollback`.
9
+ #
10
+ # This is useful for ensuring your interactor behaves atomically.
11
+ #
12
+ # @example Basic usage
13
+ # class CreateUser
14
+ # include Interactor
15
+ # include InteractorSupport::Transactionable
16
+ #
17
+ # transaction
18
+ #
19
+ # def call
20
+ # User.create!(context.user_params)
21
+ # context.fail!(message: "Simulated failure") if something_wrong?
22
+ # end
23
+ # end
24
+ #
25
+ # @see InteractorSupport::Actions
3
26
  module Transactionable
4
27
  extend ActiveSupport::Concern
5
28
  include InteractorSupport::Core
6
29
 
7
30
  included do
8
31
  class << self
32
+ # Wraps the interactor in a database transaction.
33
+ #
34
+ # If the context fails (`context.failure?`), a rollback is triggered automatically.
35
+ # You can customize the transaction behavior using standard ActiveRecord options.
36
+ #
37
+ # @param isolation [Symbol, nil] the transaction isolation level (e.g., `:read_committed`, `:serializable`)
38
+ # @param joinable [Boolean] whether this transaction can join an existing one
39
+ # @param requires_new [Boolean] whether to force a new transaction, even if one already exists
40
+ #
41
+ # @example Wrap in a basic transaction
42
+ # transaction
43
+ #
44
+ # @example With custom options
45
+ # transaction requires_new: true, isolation: :serializable
9
46
  def transaction(isolation: nil, joinable: true, requires_new: false)
10
47
  around do |interactor|
11
48
  ActiveRecord::Base.transaction(isolation: isolation, joinable: joinable, requires_new: requires_new) do
@@ -1,15 +1,41 @@
1
1
  module InteractorSupport
2
2
  module Concerns
3
+ ##
4
+ # Adds helpers for assigning and transforming values in interactor context.
5
+ #
6
+ # The `context_variable` method sets static or dynamic values before the interactor runs.
7
+ # The `transform` method allows chaining transformations (methods or lambdas) on context values.
8
+ #
9
+ # @example Assign context variables before the interactor runs
10
+ # context_variable user: -> { User.find(user_id) }, numbers: [1, 2, 3]
11
+ #
12
+ # @example Normalize email and name before using them
13
+ # transform :email, :name, with: [:strip, :downcase]
14
+ #
15
+ # @example Apply a lambda to clean up input
16
+ # transform :name, with: ->(value) { value.gsub(/\s+/, ' ').strip }
17
+ #
18
+ # @example Mixing symbols and lambdas
19
+ # transform :email, with: [:strip, :downcase, -> { email.gsub(/\s+/, '') }]
20
+ #
21
+ # @see InteractorSupport::Actions
3
22
  module Transformable
4
23
  extend ActiveSupport::Concern
5
24
  include InteractorSupport::Core
6
25
 
7
26
  included do
8
27
  class << self
9
- # context_variable first_post: Post.first
10
- # context_variable user: -> { User.find(user_id) }
11
- # context_variable items: Item.all
12
- # context_variable numbers: [1, 2, 3]
28
+ # Assigns one or more values to the context before the interactor runs.
29
+ #
30
+ # Values can be static or lazily evaluated with a lambda/proc using `instance_exec`,
31
+ # which provides access to the context and interactor instance.
32
+ #
33
+ # @param key_values [Hash{Symbol => Object, Proc}] a mapping of context keys to values or Procs
34
+ #
35
+ # @example Static and dynamic values
36
+ # context_variable first_post: Post.first
37
+ # context_variable user: -> { User.find(user_id) }
38
+ # context_variable numbers: [1, 2, 3]
13
39
  def context_variable(key_values)
14
40
  before do
15
41
  key_values.each do |key, value|
@@ -22,12 +48,40 @@ module InteractorSupport
22
48
  end
23
49
  end
24
50
 
25
- # transform :email, :name, with: [:downcase, :strip]
26
- # transform :url, with: :downcase
27
- # transform :items, with: :compact
28
- # transform :items, with: ->(value) { value.compact }
29
- # transform :email, :url, with: ->(value) { value.downcase.strip }
30
- # transform :items, with: :compact
51
+ # Transforms one or more context values using a method, a proc, or a sequence of methods.
52
+ #
53
+ # This allows simple transformations like `:strip` or `:downcase`, or more complex lambdas.
54
+ # You can also chain transformations by passing an array of method names.
55
+ #
56
+ # If a transformation fails, the context fails with an error message.
57
+ #
58
+ # @param keys [Array<Symbol>] one or more context keys to transform
59
+ # @param with [Symbol, Array<Symbol>, Proc] a single method name, an array of method names, or a proc
60
+ #
61
+ # @raise [ArgumentError] if no keys are given, or if an invalid `with:` value is passed
62
+ #
63
+ # @example Single method
64
+ # transform :email, with: :strip
65
+ #
66
+ # @example Method chain
67
+ # transform :email, with: [:strip, :downcase]
68
+ #
69
+ # @example Lambda
70
+ # transform :url, with: ->(value) { value.downcase.strip }
71
+ #
72
+ # @example Multiple keys
73
+ # transform :email, :name, with: [:downcase, :strip]
74
+ #
75
+ # @example Normalize user input
76
+ # transform :email, :name, with: [
77
+ # :strip,
78
+ # :downcase,
79
+ # ->(value) { value.gsub(/\s+/, ' ') }, # collapse duplicate spaces
80
+ # ]
81
+ #
82
+ # # Result:
83
+ # # context[:email] = "someone@example.com"
84
+ # # context[:name] = "john doe"
31
85
  def transform(*keys, with: [])
32
86
  before do
33
87
  if keys.empty?
@@ -1,11 +1,54 @@
1
1
  module InteractorSupport
2
2
  module Concerns
3
+ ##
4
+ # Adds an `update` DSL method for updating a context-loaded model with attributes.
5
+ #
6
+ # This concern allows flexible updates using data from the interactor's context.
7
+ # It supports direct mapping from context keys, nested attribute extraction from parent objects,
8
+ # lambdas for dynamic evaluation, or passing a symbol pointing to an entire context object.
9
+ #
10
+ # This is useful for updating records cleanly and consistently in declarative steps.
11
+ #
12
+ # @example Update a user using context values
13
+ # update :user, attributes: { name: :new_name, email: :new_email }
14
+ #
15
+ # @example Extract nested fields from a context object
16
+ # update :user, attributes: { form_data: [:name, :email] }
17
+ #
18
+ # @example Use a lambda for computed value
19
+ # update :post, attributes: { published_at: -> { Time.current } }
20
+ #
21
+ # @see InteractorSupport::Actions
3
22
  module Updatable
4
23
  extend ActiveSupport::Concern
5
24
  include InteractorSupport::Core
6
25
 
7
26
  included do
8
27
  class << self
28
+ # Updates a model using values from the context before the interactor runs.
29
+ #
30
+ # Supports flexible ways of specifying attributes:
31
+ # - A hash mapping attribute names to context keys, nested keys, or lambdas
32
+ # - A symbol pointing to a hash in context
33
+ #
34
+ # If the record or required data is missing, the context fails with an error.
35
+ #
36
+ # @param model [Symbol] the key in the context holding the record to update
37
+ # @param attributes [Hash, Symbol] a hash mapping attributes to context keys/lambdas, or a symbol pointing to a context hash
38
+ # @param context_key [Symbol, nil] key to assign the updated record to in context (defaults to `model`)
39
+ #
40
+ # @example Basic attribute update using context keys
41
+ # update :user, attributes: { name: :new_name, email: :new_email }
42
+ #
43
+ # @example Use a lambda for dynamic value
44
+ # update :post, attributes: { published_at: -> { Time.current } }
45
+ #
46
+ # @example Nested context value lookup from a parent object
47
+ # # Assuming context[:form_data] = OpenStruct.new(name: "Hi", email: "hi@example.com")
48
+ # update :user, attributes: { form_data: [:name, :email] }
49
+ #
50
+ # @example Using a symbol to fetch all attributes from another context object
51
+ # update :order, attributes: :order_attributes
9
52
  def update(model, attributes: {}, context_key: nil)
10
53
  context_key ||= model
11
54
 
@@ -46,7 +89,6 @@ module InteractorSupport
46
89
 
47
90
  record.update!(update_data)
48
91
 
49
- # Assign the updated record to context
50
92
  context[context_key] = record
51
93
  end
52
94
  end
@@ -1,17 +1,40 @@
1
1
  module InteractorSupport
2
+ ##
3
+ # Global configuration for InteractorSupport.
4
+ #
5
+ # This allows customization of how request objects behave when used in interactors.
6
+ #
7
+ # @example Set custom behavior
8
+ # InteractorSupport.configuration.request_object_behavior = :returns_self
9
+ # InteractorSupport.configuration.request_object_key_type = :struct
10
+ #
11
+ # @see InteractorSupport.configuration
2
12
  class Configuration
3
- attr_accessor :request_object_behavior, :request_object_key_type
13
+ ##
14
+ # Defines how request objects behave when called.
15
+ #
16
+ # - `:returns_context` — The request object returns an Interactor-style context.
17
+ # - `:returns_self` — The request object returns itself, allowing method chaining.
18
+ #
19
+ # @return [:returns_context, :returns_self]
20
+ attr_accessor :request_object_behavior
4
21
 
22
+ ##
23
+ # Defines the key type used in request object context when `:returns_context` is active.
24
+ #
25
+ # - `:string` — Keys are string-based (`"name"`)
26
+ # - `:symbol` — Keys are symbol-based (`:name`)
27
+ # - `:struct` — Keys are accessed via struct-style method calls (`name`)
28
+ #
29
+ # @return [:string, :symbol, :struct]
30
+ attr_accessor :request_object_key_type
31
+
32
+ ##
33
+ # Initializes the configuration with default values:
34
+ # - `request_object_behavior` defaults to `:returns_context`
35
+ # - `request_object_key_type` defaults to `:symbol`
5
36
  def initialize
6
- # Default configuration values.
7
- # :returns_context - request objects return a context object.
8
- # :returns_self - request objects return self.
9
37
  @request_object_behavior = :returns_context
10
-
11
- # Default configuration values, only applies when request_object_behavior is :returns_context.
12
- # :string - request object keys are strings.
13
- # :symbol - request object keys are symbols.
14
- # :struct - request object keys are struct objects.
15
38
  @request_object_key_type = :symbol
16
39
  end
17
40
  end