interactor_support 1.0.1 → 1.0.3

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: 99b9bd6793db476faba16087e24a939982b13974853c2a022e865e011509b143
4
- data.tar.gz: 6980cdf3351816cbd8209825c03b58b82c195ac21c337d0638223a97c68814f8
3
+ metadata.gz: 2b479b98d9029e0865ab4526efcc08ec47736ecba96547d2ab9be294f54d2880
4
+ data.tar.gz: 74f5d2a0024afb6b7c49ff153a339d6373d25072d37e546827407e65cfd0bc8d
5
5
  SHA512:
6
- metadata.gz: ce6d5d43e59d7a70b8fa5e2183da93fc52644de5b12c796cf9b617e5a5eb5d78b32439e884cc40860e1bf2e7da6d1a926b58f9ed3257d2ced5ceb77a00c75715
7
- data.tar.gz: 940bafa5fbf6d6178b0c8f5f603ff37995c178511aab9cb50d12acc15492a9ace497dbd1e66b76c12fbc3e6943d41d97a9ec557d5451bd3bb6e741e590316194
6
+ metadata.gz: e1d24d1b7fe20316f26d14f029387d90e1c88e2dc9654500dd40ae9546e030121efcb5de991d091b752308f0f976533a4dabaebc3cb2bebc269b7f5ca70ab7d3
7
+ data.tar.gz: 32c320144c9f90dc53769bd104a308d17ce663e8618cc0f55f8435d89835ed3aa2cb3049df023422de0d34068ced12795e12127e8eb976f18a3db8a56c0e759a
data/.prettierignore ADDED
@@ -0,0 +1 @@
1
+ .yardopts
data/.yardopts ADDED
@@ -0,0 +1,19 @@
1
+ lib/interactor_support.rb
2
+ lib/interactor_support/**/*.rb
3
+
4
+ --title
5
+ InteractorSupport Documentation
6
+
7
+ --markup
8
+ markdown
9
+
10
+ --no-private
11
+ --no-yardopts
12
+
13
+ --exclude
14
+ bin/
15
+ spec/
16
+ test/
17
+ config/
18
+ db/
19
+ vendor/
data/CHANGELOG.md CHANGED
@@ -7,3 +7,13 @@
7
7
  ## [1.0.1] - 2025-03-26
8
8
 
9
9
  - Removed runtime requirements for rails and interactor.
10
+
11
+ ## [1.0.2] - 2025-03-28
12
+
13
+ - Added support for mixing symbols and procs in the transformable concern
14
+
15
+ ## [1.0.3] - 2025-04-02
16
+
17
+ - Added support for rewriting attribute names in a request object
18
+ - Better support for type coersion, using Active model + Array, Hash, and Symbol
19
+ - Better support for `AnyClass` type validations
data/README.md CHANGED
@@ -28,7 +28,7 @@ Make your **Rails interactors** clean, powerful, and less error-prone!
28
28
  Add to your Gemfile:
29
29
 
30
30
  ```sh
31
- bundle add interactor_support
31
+ gem 'interactor_support', '~> 1.0', '>= 1.0.1'
32
32
  ```
33
33
 
34
34
  Or install manually:
@@ -84,7 +84,7 @@ class UpdateTodoTitle
84
84
  include Interactor
85
85
  include InteractorSupport
86
86
 
87
- requires :todo_id, :title
87
+ required :todo_id, :title
88
88
  transform :title, with: :strip
89
89
  find_by :todo, query: { id: :todo_id }, required: true
90
90
  update :todo, attributes: { title: :title }
@@ -110,7 +110,7 @@ class CompleteTodo
110
110
  include InteractorSupport
111
111
 
112
112
  transaction
113
- requires :todo_id
113
+ required :todo_id
114
114
  find_by :todo, query: { id: :todo_id }, required: true
115
115
 
116
116
  update :todo, attributes: {
@@ -133,7 +133,7 @@ class CompleteTodo
133
133
  include InteractorSupport
134
134
 
135
135
  transaction
136
- requires :todo
136
+ required :todo
137
137
  skip if: -> { todo.completed? }
138
138
  update :todo, attributes: { completed: true, completed_at: -> { Time.current } }
139
139
  end
@@ -235,10 +235,53 @@ find_where :post, where: { user_id: :user_id }, context_key: :user_posts
235
235
 
236
236
  Provides `transform` to **sanitize and normalize inputs**.
237
237
 
238
+ ```rb
239
+ # any method that the attribute responds to will work
240
+ transform :title, with: :strip
241
+
242
+ # You can chain transformers on an attribute
243
+ # show_the_thing == "1" => "1".to_i.positive? => true
244
+ transform :show_the_thing, with: [:to_i, :positive?]
245
+
246
+ # transforming a string to a boolean using a lambda, eg: "true" => true
247
+ transform :my_param, with: -> (val) { ActiveModel::Type::Boolean.new.cast(val) }
248
+
249
+ # added in 1.0.2
250
+ # mixing symbols and keys. eg: " True " => true
251
+ transform :my_param, with: [
252
+ :strip,
253
+ :downcase,
254
+ -> (val) { ActiveModel::Type::Boolean.new.cast(val) }
255
+ ]
256
+
257
+ ```
258
+
238
259
  #### 🔹 **`InteractorSupport::Concerns::Skippable`**
239
260
 
240
261
  Allows an interactor to **skip execution** if a condition is met.
241
262
 
263
+ ```rb
264
+ # skips execution
265
+ skip if: true
266
+ # skips execution when a lambda is passed
267
+ skip if: -> { true }
268
+ # using a method
269
+ skip if: :some_method?
270
+ # using a context variable
271
+ skip if: :condition
272
+
273
+ # Using `unless`
274
+
275
+ # skips execution
276
+ skip unless: false
277
+ # skips execution when a lambda is passed
278
+ skip unless: -> { false }
279
+ # using a method
280
+ skip unless: :some_method?
281
+ # using a context variable
282
+ skip unless: :condition
283
+ ```
284
+
242
285
  #### 🔹 **`InteractorSupport::Validations`**
243
286
 
244
287
  Provides **automatic input validation** before execution. This includes `ActiveModel::Validations` and
@@ -272,7 +315,217 @@ If any validation fails, context.fail!(errors: errors.full_messages) will automa
272
315
 
273
316
  #### 🔹 **`InteractorSupport::RequestObject`**
274
317
 
275
- Provides structured, validated request objects based on **ActiveModel**.
318
+ A flexible, form-like abstraction for service object inputs, built on top of ActiveModel. InteractorSupport::RequestObject extends ActiveModel::Model and ActiveModel::Validations to provide structured, validated, and transformed input objects. It adds first-class support for nested objects, type coercion, attribute transformation, and array handling. It's ideal for use with any architecture that benefits from strong input modeling.
319
+
320
+ _RequestObject Enforces Input Integrity, and 🔐 allow-lists attributes by default_
321
+
322
+ **Features**
323
+
324
+ - Define attributes with types and transformation pipelines
325
+ - Supports primitive and custom object types
326
+ - Deeply nested input coercion and validation
327
+ - Array support for any type
328
+ - Auto-generated context hashes or structs
329
+ - Key rewriting for internal/external mapping
330
+ - Full ActiveModel validation support
331
+
332
+ Rather than manually massaging and validating hashes or params in your services, define intent-driven objects that:
333
+
334
+ - clean incoming values
335
+ - validate data structure and content
336
+ - expose clean interfaces for business logic
337
+
338
+ 🚀 Getting Started
339
+
340
+ 1. Define a Request Object
341
+
342
+ ```rb
343
+ class GenreRequest
344
+ include InteractorSupport::RequestObject
345
+
346
+ attribute :title, transform: :strip
347
+ attribute :description, transform: :strip
348
+
349
+ validates :title, :description, presence: true
350
+ end
351
+ ```
352
+
353
+ 2. Use it in your Interactor, Service, or Controller
354
+
355
+ ```rb
356
+ class GenresController < ApplicationController
357
+ def create
358
+ context = SomeOrganizerForCreatingGenres.call(
359
+ GenreRequest.new(params.permit!) # 😉 request objects are a safe and powerful replacement for strong params
360
+ )
361
+
362
+ # render context.genre & handle success? vs failure?
363
+ end
364
+ end
365
+ ```
366
+
367
+ ## Attribute Features
368
+
369
+ #### Transformations:
370
+
371
+ Apply one or more transformations when values are assigned.
372
+
373
+ ```rb
374
+ attribute :email, transform: [:strip, :downcase]
375
+ ```
376
+
377
+ - You can use any transform that the value can `respond_to?`
378
+ - Define custom transforms as instance methods.
379
+
380
+ Type Casting:
381
+ Cast inputs to expected types automatically:
382
+
383
+ ```rb
384
+ attribute :age, type: :integer
385
+ attribute :tags, type: :string, array: true
386
+ attribute :config, type: Hash
387
+ attribute :published_at, type: :datetime
388
+ attribute :user, type: User
389
+ ```
390
+
391
+ If the value is already of the expected type, it will just pass through. Otherwise, it will try to cast it.
392
+ If casting fails, or you specify an unsupported type, it will raise an `InteractorSupport::RequestObject::TypeError`
393
+
394
+ Supported types are
395
+
396
+ - Any ActiveModel::Type, provided as a symbol.
397
+ - The following primitives, Array, Hash, Symbol
398
+ - RequestObject subclasses (for nesting request objects)
399
+
400
+ #### Nesting Request Objects
401
+
402
+ ```rb
403
+ class AuthorRequest
404
+ include InteractorSupport::RequestObject
405
+
406
+ attribute :name
407
+ attribute :location, type: LocationRequest
408
+ end
409
+
410
+ class PostRequest
411
+ include InteractorSupport::RequestObject
412
+
413
+ attribute :authors, type: AuthorRequest, array: true
414
+ end
415
+ ```
416
+
417
+ Nested objects are instantiated recursively and validated automatically.
418
+
419
+ ## Rewrite Keys
420
+
421
+ Rename external keys for internal use.
422
+
423
+ ```rb
424
+ attribute :image, rewrite: :image_url, transform: :strip
425
+
426
+ request = ImageUploadRequest.new(image: ' https://url.com ')
427
+ request.image_url # => "https://url.com"
428
+ request.respond_to?(:image) # => false
429
+ ```
430
+
431
+ ## to_context Output
432
+
433
+ Return a nested Hash, Struct, or self:
434
+
435
+ ```rb
436
+ # Default
437
+ PostRequest.new(authors: [{ name: "Ruby", location: { city: "Seattle" }}])
438
+ # returns a hash with symbol keys => {:authors=>[{:name=>"Ruby", :location=>{:city=>"Seattle"}}]}
439
+
440
+ # Configure globally
441
+ InteractorSupport.configure do |config|
442
+ config.request_object_behavior = :returns_context # or :returns_self
443
+ config.request_object_key_type = :symbol # or :string, :struct
444
+ end
445
+
446
+ # request_object_behavior = :returns_context, request_object_key_type = :string
447
+ PostRequest.new(authors: [{ name: "Ruby", location: { city: "Seattle" }}])
448
+ # returns a hash with string keys => {"authors"=>[{"name"=>"Ruby", "location"=>{"city"=>"Seattle"}}]}
449
+
450
+ # request_object_behavior = :returns_context, request_object_key_type = :struct
451
+ PostRequest.new(authors: [{ name: "Ruby", location: { city: "Seattle" }}])
452
+ # returns a Struct => #<struct authors=[{:name=>"Ruby", :location=>{:city=>"Seattle"}}]>
453
+
454
+ # request_object_behavior = :returns_self, request_object_key_type = :symbol
455
+ request = PostRequest.new(authors: [{ name: "Ruby", location: { city: "Seattle" }}])
456
+ # returns the request object => #<PostRequest authors=[{:name=>"Ruby", :location=>{:city=>"Seattle"}}]>
457
+ # request.authors.first.location.city => "Seattle"
458
+ # request.to_context => {:authors=>[{:name=>"Ruby", :location=>{:city=>"Seattle"}}]}
459
+ ```
460
+
461
+ 🛡 Replacing Strong Parameters Safely
462
+
463
+ InteractorSupport::RequestObject is a safe, testable, and expressive alternative to Rails’ strong_parameters. While strong_params are great for sanitizing controller input, they tend to:
464
+
465
+ - Leak into your business logic
466
+ - Lack structure and type safety
467
+ - Require repetitive permit/require declarations
468
+ - Get clumsy with nesting and arrays
469
+
470
+ Instead, RequestObject defines the expected shape and behavior of input once, and gives you:
471
+
472
+ - Input sanitization via transform:
473
+ - Validation via ActiveModel
474
+ - Type coercion (including arrays and nesting)
475
+ - Reusable, composable input classes
476
+
477
+ StrongParams Example
478
+
479
+ ```rb
480
+ def user_params
481
+ params.require(:user).permit(:name, :email, :age)
482
+ end
483
+
484
+ def create
485
+ user = User.new(user_params)
486
+ ...
487
+ end
488
+ ```
489
+
490
+ Even with this, you still have to:
491
+ • Validate formats (like email)
492
+ • Coerce types (:age is still a string!)
493
+ • Repeat this logic elsewhere
494
+
495
+ **Request Object Equivelent**
496
+
497
+ ```rb
498
+ class UserRequest
499
+ include InteractorSupport::RequestObject
500
+
501
+ attribute :name, transform: :strip
502
+ attribute :email, transform: [:strip, :downcase]
503
+ attribute :age, type: :integer # or transform: [:to_i]
504
+
505
+ validates :name, presence: true
506
+ validates_format_of :email, with: URI::MailTo::EMAIL_REGEXP
507
+ end
508
+ ```
509
+
510
+ **Why replace Strong Params?**
511
+ | Feature | Strong Params | Request Object |
512
+ |----------------------------------|---------------------|----------------------|
513
+ | Requires manual permit/require | ✅ Yes | ❌ Not needed |
514
+ | Validates types/formats | ❌ No | ✅ Yes |
515
+ | Handles nested objects | 😬 With effort | ✅ First-class support |
516
+ | Works outside controllers | ❌ Not cleanly | ✅ Perfect for services/interactors |
517
+ | Self-documenting input shape | ❌ No | ✅ Defined via attribute DSL |
518
+ | Testable as a unit | ❌ Not directly | ✅ Easily tested like a form object |
519
+
520
+ 💡 Tip
521
+
522
+ You can still use params.require(...).permit(...) in the controller if you want to restrict top-level keys, then pass that sanitized hash to your RequestObject:
523
+
524
+ ```rb
525
+ UserRequest.new(params.require(:user).permit(:name, :email, :age))
526
+ ```
527
+
528
+ But with RequestObject, that’s often unnecessary because you’re already defining a schema.
276
529
 
277
530
  ## 🤝 **Contributing**
278
531
 
@@ -1,4 +1,35 @@
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
+ # - {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
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
+ # @see InteractorSupport::Concerns::Skippable
29
+ # @see InteractorSupport::Concerns::Transactionable
30
+ # @see InteractorSupport::Concerns::Updatable
31
+ # @see InteractorSupport::Concerns::Findable
32
+ # @see InteractorSupport::Concerns::Transformable
2
33
  module Actions
3
34
  extend ActiveSupport::Concern
4
35
  included do
@@ -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?
@@ -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