interactor_support 1.0.2 β†’ 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: 9389f81b3832c245eac6c1f850a1b55c6baaebbc01be44eccd8d4f31c45bc731
4
- data.tar.gz: c3ee7cd401d9d744e51208bc9763cb417e25410e4e2797bb51676aece17ffeec
3
+ metadata.gz: 2b479b98d9029e0865ab4526efcc08ec47736ecba96547d2ab9be294f54d2880
4
+ data.tar.gz: 74f5d2a0024afb6b7c49ff153a339d6373d25072d37e546827407e65cfd0bc8d
5
5
  SHA512:
6
- metadata.gz: 67218c44b1b89e1173abba43d298742eaa2bf59d7a884c5e65017c855f80a4ace05a2423232325395085f8e70c9d7f981e6ef39d267368e9eaf7c2c32fb7e57e
7
- data.tar.gz: 0c8ff1bac440c0cd7f07d4e3f9463109986ce92240aaa5725e5d405f21c62040d6ea7509e1de2b951184db39cef977c181f01c73ed69895e599b67dda093628c
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
@@ -11,3 +11,9 @@
11
11
  ## [1.0.2] - 2025-03-28
12
12
 
13
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
@@ -315,9 +315,29 @@ If any validation fails, context.fail!(errors: errors.full_messages) will automa
315
315
 
316
316
  #### πŸ”Ή **`InteractorSupport::RequestObject`**
317
317
 
318
- 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
319
 
320
- For now, here's the specs:
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
321
341
 
322
342
  ```rb
323
343
  class GenreRequest
@@ -328,347 +348,185 @@ class GenreRequest
328
348
 
329
349
  validates :title, :description, presence: true
330
350
  end
351
+ ```
331
352
 
332
- class LocationRequest
333
- include InteractorSupport::RequestObject
334
-
335
- attribute :city, transform: [:downcase, :strip]
336
- attribute :country_code, transform: [:strip, :upcase]
337
- attribute :state_code, transform: [:strip, :upcase]
338
- attribute :postal_code, transform: [:strip, :clean_postal_code]
339
- attribute :address, transform: :strip
353
+ 2. Use it in your Interactor, Service, or Controller
340
354
 
341
- validates :city, :postal_code, :address, presence: true
342
- validates :country_code, :state_code, presence: true, length: { is: 2 }
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
+ )
343
361
 
344
- def clean_postal_code(value)
345
- value.to_s.gsub(/\D/, '')
346
- value.first(5)
362
+ # render context.genre & handle success? vs failure?
347
363
  end
348
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.
349
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
350
403
  class AuthorRequest
351
404
  include InteractorSupport::RequestObject
352
405
 
353
- attribute :name, transform: :strip
354
- attribute :email, transform: [:strip, :downcase]
355
- attribute :age, transform: :to_i
406
+ attribute :name
356
407
  attribute :location, type: LocationRequest
357
- validates_format_of :email, with: URI::MailTo::EMAIL_REGEXP
358
408
  end
359
409
 
360
410
  class PostRequest
361
411
  include InteractorSupport::RequestObject
362
412
 
363
- attribute :user_id
364
- attribute :title, transform: :strip
365
- attribute :content, transform: :strip
366
- attribute :genre, type: GenreRequest
367
413
  attribute :authors, type: AuthorRequest, array: true
368
414
  end
415
+ ```
369
416
 
370
- RSpec.describe(InteractorSupport::RequestObject) do
371
- describe 'attribute transformation' do
372
- before do
373
- InteractorSupport.configure do |config|
374
- config.request_object_behavior = :returns_self
375
- end
376
- end
377
- context 'when using a single transform' do
378
- it 'strips the value for a symbol transform' do
379
- genre = GenreRequest.new(title: ' Science Fiction ', description: ' A genre of speculative fiction ')
380
- expect(genre.title).to(eq('Science Fiction'))
381
- expect(genre.description).to(eq('A genre of speculative fiction'))
382
- end
383
- end
384
-
385
- context 'when using an array of transforms' do
386
- it 'applies all transform methods in order' do
387
- buyer = LocationRequest.new(
388
- city: ' New York ',
389
- country_code: ' us ',
390
- state_code: ' ny ',
391
- postal_code: ' 10001-5432 ',
392
- address: ' 123 Main St. ',
393
- )
394
- expect(buyer.city).to(eq('new york'))
395
- expect(buyer.country_code).to(eq('US'))
396
- expect(buyer.state_code).to(eq('NY'))
397
- expect(buyer.postal_code).to(eq('10001'))
398
- expect(buyer.address).to(eq('123 Main St.'))
399
- end
400
- end
401
-
402
- context 'when the value does not respond to a transform method' do
403
- it 'raises and argument error if the transform method is not defined' do
404
- class DummyRequest
405
- include InteractorSupport::RequestObject
406
- attribute :number, transform: :strip
407
- validates :number, presence: true
408
- end
409
-
410
- expect { DummyRequest.new(number: 1234) }.to(raise_error(ArgumentError))
411
- end
412
- end
413
- end
417
+ Nested objects are instantiated recursively and validated automatically.
414
418
 
415
- describe 'nesting request objects and array support' do
416
- before do
417
- InteractorSupport.configure do |config|
418
- config.request_object_behavior = :returns_self
419
- end
420
- end
421
- context 'when using a type without array' do
422
- it "wraps the given hash in the type's new instance" do
423
- post = PostRequest.new(
424
- user_id: 1,
425
- title: ' My First Post ',
426
- content: ' This is the content of my first post ',
427
- genre: { title: ' Science Fiction ', description: ' A genre of speculative fiction ' },
428
- authors: [
429
- {
430
- name: ' John Doe ',
431
- email: 'me@mail.com',
432
- age: ' 25 ',
433
- location: {
434
- city: ' New York ',
435
- country_code: ' us ',
436
- state_code: ' ny ',
437
- postal_code: ' 10001-5432 ',
438
- address: ' 123 Main St. ',
439
- },
440
- },
441
- {
442
- name: ' Jane Doe ',
443
- email: 'you@mail.com',
444
- age: ' 25 ',
445
- location: {
446
- city: ' Los Angeles ',
447
- country_code: ' us ',
448
- state_code: ' ca ',
449
- postal_code: ' 90001 ',
450
- address: ' 456 Elm St. ',
451
- },
452
- },
453
- ],
454
- )
455
-
456
- expect(post).to(be_valid)
457
- expect(post.user_id).to(eq(1))
458
- expect(post.title).to(eq('My First Post'))
459
- expect(post.content).to(eq('This is the content of my first post'))
460
- expect(post.genre).to(be_a(GenreRequest))
461
- expect(post.genre.title).to(eq('Science Fiction'))
462
- expect(post.genre.description).to(eq('A genre of speculative fiction'))
463
- expect(post.authors).to(be_an(Array))
464
- expect(post.authors.size).to(eq(2))
465
- post.authors.each do |author|
466
- expect(author).to(be_a(AuthorRequest))
467
- expect(author).to(be_valid)
468
- expect(author.location).to(be_a(LocationRequest))
469
- expect(author.location).to(be_valid)
470
- expect(author.location.city).to(be_in(['new york', 'los angeles']))
471
- expect(author.location.country_code).to(eq('US'))
472
- expect(author.location.state_code).to(be_in(['NY', 'CA']))
473
- expect(author.location.postal_code).to(be_in(['10001', '90001']))
474
- expect(author.location.address).to(be_in(['123 Main St.', '456 Elm St.']))
475
- end
476
- end
477
- end
478
- end
419
+ ## Rewrite Keys
479
420
 
480
- describe 'to_context' do
481
- before do
482
- InteractorSupport.configure do |config|
483
- config.request_object_behavior = :returns_self
484
- end
485
- end
486
- it 'returns a struct and includes nested attributes' do
487
- context = PostRequest.new(
488
- user_id: 1,
489
- title: ' My First Post ',
490
- content: ' This is the content of my first post ',
491
- genre: { title: ' Science Fiction ', description: ' A genre of speculative fiction ' },
492
- authors: [
493
- {
494
- name: ' John Doe ',
495
- email: 'a@b.com',
496
- age: ' 25 ',
497
- location: {
498
- city: ' New York ',
499
- country_code: ' us ',
500
- state_code: ' ny ',
501
- postal_code: ' 10001-5432 ',
502
- address: ' 123 Main St. ',
503
- },
504
- },
505
- ],
506
- ).to_context
507
-
508
- expect(context).to(be_a(Hash))
509
- expect(context[:user_id]).to(eq(1))
510
- expect(context[:title]).to(eq('My First Post'))
511
- expect(context[:content]).to(eq('This is the content of my first post'))
512
- expect(context[:genre]).to(be_a(Hash))
513
- expect(context.dig(:genre, :title)).to(eq('Science Fiction'))
514
- expect(context.dig(:genre, :description)).to(eq('A genre of speculative fiction'))
515
- expect(context[:authors]).to(be_an(Array))
516
- expect(context[:authors].size).to(eq(1))
517
- author = context[:authors].first
518
- expect(author).to(be_a(Hash))
519
- expect(author[:name]).to(eq('John Doe'))
520
- expect(author[:email]).to(eq('a@b.com'))
521
- expect(author[:age]).to(eq(25))
522
- expect(author[:location]).to(be_a(Hash))
523
- expect(author[:location][:city]).to(eq('new york'))
524
- expect(author[:location][:country_code]).to(eq('US'))
525
- end
526
- end
421
+ Rename external keys for internal use.
527
422
 
528
- describe 'configured request object behavior' do
529
- it 'returns a context hash with symbol keys when configured to do so' do
530
- InteractorSupport.configure do |config|
531
- config.request_object_behavior = :returns_context
532
- config.request_object_key_type = :symbol
533
- end
534
-
535
- context = PostRequest.new(
536
- user_id: 1,
537
- title: ' My First Post ',
538
- content: ' This is the content of my first post ',
539
- genre: { title: ' Science Fiction ', description: ' A genre of speculative fiction ' },
540
- authors: [
541
- {
542
- name: ' John Doe ',
543
- email: 'j@j.com',
544
- age: ' 25 ',
545
- location: {
546
- city: ' New York ',
547
- country_code: ' us ',
548
- state_code: ' ny ',
549
- postal_code: ' 10001-5432 ',
550
- address: ' 123 Main St. ',
551
- },
552
- },
553
- ],
554
- )
555
-
556
- expect(context).to(be_a(Hash))
557
- expect(context[:user_id]).to(eq(1))
558
- expect(context[:title]).to(eq('My First Post'))
559
- expect(context[:content]).to(eq('This is the content of my first post'))
560
- expect(context[:genre]).to(be_a(Hash))
561
- expect(context.dig(:genre, :title)).to(eq('Science Fiction'))
562
- expect(context.dig(:genre, :description)).to(eq('A genre of speculative fiction'))
563
- expect(context[:authors]).to(be_an(Array))
564
- expect(context[:authors].size).to(eq(1))
565
- author = context[:authors].first
566
- expect(author).to(be_a(Hash))
567
- expect(author[:name]).to(eq('John Doe'))
568
- expect(author[:email]).to(eq('j@j.com'))
569
- expect(author[:age]).to(eq(25))
570
- expect(author[:location]).to(be_a(Hash))
571
- expect(author[:location][:city]).to(eq('new york'))
572
- expect(author[:location][:country_code]).to(eq('US'))
573
- end
574
-
575
- it 'returns a context hash with string keys when configured to do so' do
576
- InteractorSupport.configure do |config|
577
- config.request_object_behavior = :returns_context
578
- config.request_object_key_type = :string
579
- end
580
-
581
- post = PostRequest.new(
582
- user_id: 1,
583
- title: ' My First Post ',
584
- content: ' This is the content of my first post ',
585
- genre: { title: ' Science Fiction ', description: ' A genre of speculative fiction ' },
586
- authors: [
587
- {
588
- name: ' John Doe ',
589
- email: 'j@j.com',
590
- age: ' 25 ',
591
- location: {
592
- city: ' New York ',
593
- country_code: ' us ',
594
- state_code: ' ny ',
595
- postal_code: ' 10001-5432 ',
596
- address: ' 123 Main St. ',
597
- },
598
- },
599
- ],
600
- )
601
-
602
- context = post
603
- expect(context).to(be_a(Hash))
604
- expect(context['user_id']).to(eq(1))
605
- expect(context['title']).to(eq('My First Post'))
606
- expect(context['content']).to(eq('This is the content of my first post'))
607
- expect(context['genre']).to(be_a(Hash))
608
- expect(context.dig('genre', 'title')).to(eq('Science Fiction'))
609
- expect(context.dig('genre', 'description')).to(eq('A genre of speculative fiction'))
610
- expect(context['authors']).to(be_an(Array))
611
- expect(context['authors'].size).to(eq(1))
612
- author = context['authors'].first
613
- expect(author).to(be_a(Hash))
614
- expect(author['name']).to(eq('John Doe'))
615
- expect(author['email']).to(eq('j@j.com'))
616
- expect(author['age']).to(eq(25))
617
- expect(author['location']).to(be_a(Hash))
618
- expect(author['location']['city']).to(eq('new york'))
619
- expect(author['location']['country_code']).to(eq('US'))
620
- end
621
-
622
- it 'returns a context struct when configured to do so' do
623
- InteractorSupport.configure do |config|
624
- config.request_object_behavior = :returns_context
625
- config.request_object_key_type = :struct
626
- end
627
-
628
- post = PostRequest.new(
629
- user_id: 1,
630
- title: ' My First Post ',
631
- content: ' This is the content of my first post ',
632
- genre: { title: ' Science Fiction ', description: ' A genre of speculative fiction ' },
633
- authors: [
634
- {
635
- name: ' John Doe ',
636
- email: 'j@j.com',
637
- age: ' 25 ',
638
- location: {
639
- city: ' New York ',
640
- country_code: ' us ',
641
- state_code: ' ny ',
642
- postal_code: ' 10001-5432 ',
643
- address: ' 123 Main St. ',
644
- },
645
- },
646
- ],
647
- )
648
-
649
- context = post
650
- expect(context).to(be_a(Struct))
651
- expect(context.user_id).to(eq(1))
652
- expect(context.title).to(eq('My First Post'))
653
- expect(context.content).to(eq('This is the content of my first post'))
654
- expect(context.genre).to(be_a(Struct))
655
- expect(context.genre.title).to(eq('Science Fiction'))
656
- expect(context.genre.description).to(eq('A genre of speculative fiction'))
657
- expect(context.authors).to(be_an(Array))
658
- expect(context.authors.size).to(eq(1))
659
- author = context.authors.first
660
- expect(author).to(be_a(Struct))
661
- expect(author.name).to(eq('John Doe'))
662
- expect(author.email).to(eq('j@j.com'))
663
- expect(author.age).to(eq(25))
664
- expect(author.location).to(be_a(Struct))
665
- expect(author.location.city).to(eq('new york'))
666
- expect(author.location.country_code).to(eq('US'))
667
- end
668
- end
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
+ ...
669
487
  end
670
488
  ```
671
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.
529
+
672
530
  ## 🀝 **Contributing**
673
531
 
674
532
  Pull requests are welcome on [GitHub](https://github.com/charliemitchell/interactor_support).
@@ -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