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.
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,344 +348,307 @@ 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.
349
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
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
+ ...
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.
529
+
530
+ ## InteractorSupport::Organizable
531
+
532
+ 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.
533
+
534
+ Features
535
+
536
+ - organize: Call interactors with request objects, optionally namespaced under a context_key.
537
+ - request_params: Extract, shape, filter, rename, flatten, and merge incoming params in a clear and declarative way.
538
+ - Built for controllers or service entry points.
539
+ - Rails-native feel β€” works seamlessly with strong params.
540
+
541
+ #### API Reference
542
+
543
+ **#organize(interactor, params:, request_object:, context_key: nil)**
544
+ Calls the given interactor with a request object built from the provided params.
545
+
546
+ | Argument | Type | Description |
547
+ | -------------- | ------------- | --------------------------------------------------------------------------- |
548
+ | interactor | Class | The interactor to call (.call must be defined). |
549
+ | params | Hash | Parameters passed to the request object. |
550
+ | request_object | Class | A request object class that accepts params in its initializer. |
551
+ | context_key | Symbol or nil | Optional key to namespace the request object inside the interactor context. |
552
+
553
+ Examples
554
+
555
+ ```rb
556
+ organize(MyInteractor, params: request_params, request_object: MyRequest)
557
+
558
+ # => MyInteractor.call(MyRequest.new(params))
559
+
560
+ organize(MyInteractor, params: request_params, request_object: MyRequest, context_key: :request)
561
+
562
+ # => MyInteractor.call({ request: MyRequest.new(params) })
563
+ ```
564
+
565
+ #### #request_params(\*top_level_keys, merge: {}, except: [], rewrite: [])
566
+
567
+ 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.
568
+
569
+ | Argument | Type | Description |
570
+ | ----------------- | -------------------------------- | ------------------------------------------------------------------------- |
571
+ | `*top_level_keys` | `Symbol...` | Optional list of top-level keys to include. If omitted, includes all. |
572
+ | `merge:` | `Hash` | Extra values to merge into the result. |
573
+ | `except:` | `Array<Symbol or Array<Symbol>>` | Keys or nested key paths to exclude. |
574
+ | `rewrite:` | `Array<Hash>` | Rules for renaming, flattening, filtering, merging, or defaulting values. |
575
+
576
+ Rewrite Options
577
+
578
+ Each rewrite entry is a hash in the form { key => options }, where options may include:
579
+ | Option | Type | Description |
580
+ |-----------|---------------------------|-------------------------------------------------------------------|
581
+ | `as` | `Symbol` | Rename the key to a new top-level key. |
582
+ | `only` | `Array<Symbol>` | Include only these subkeys in the result. |
583
+ | `except` | `Array<Symbol>` | Remove these subkeys from the result. |
584
+ | `flatten` | `true` or `Array<Symbol>` | Flatten all subkeys into top-level (or just the specified ones). |
585
+ | `default` | `Hash` | Use this value if the original key is missing or nil. |
586
+ | `merge` | `Hash` | Merge this hash into the result (after filtering and flattening). |
587
+
588
+ Example: full usage
589
+
590
+ ```rb
591
+ # Incoming params:
592
+ params = {
593
+ order: {
594
+ product_id: 1,
595
+ quantity: 2,
596
+ internal: "should be removed"
597
+ },
598
+ metadata: {
599
+ source: "mobile",
600
+ internal: "hidden",
601
+ location: { ip: "1.2.3.4" }
602
+ },
603
+ flags: {
604
+ foo: true
605
+ },
606
+ internal: "global_internal",
607
+ session: nil
608
+ }
609
+
610
+ # Incantation:
611
+ request_params(:order, :metadata, :flags, :session,
612
+ merge: { user: current_user }, # <- Add the user
613
+ except: [[:order, :internal], :internal], # <- remove `order.internal`, and the top level key `internal`
614
+ rewrite: [
615
+ { order: { flatten: true } }, # <- moves all the values from order to top level keys
616
+ { metadata: { as: :meta, only: [:source, :location], flatten: [:location] } }, # <- Rename metadata to meta, pluck source and location, move location's values to meta
617
+ { flags: { merge: { debug: true } } }, # <- add flags.debug = true
618
+ { session: { default: { id: nil } } } # <- create a default value for session
619
+ ]
620
+ )
621
+
622
+ # Result
623
+ {
624
+ product_id: 1,
625
+ quantity: 2,
626
+ meta: {
627
+ source: "mobile",
628
+ ip: "1.2.3.4"
629
+ },
630
+ flags: {
631
+ foo: true,
632
+ debug: true
633
+ },
634
+ session: {
635
+ id: nil
636
+ },
637
+ user: current_user
638
+ }
639
+ ```
640
+
641
+ ⚠️ Array flattening is not supported
642
+
643
+ 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.
644
+
645
+ **Usage**
646
+
647
+ Include in a controller or service base class
648
+
649
+ ```rb
650
+ class ApplicationController < ActionController::Base
651
+ include InteractorSupport::Concerns::Organizable
669
652
  end
670
653
  ```
671
654