senro_usecaser 0.1.0

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 ADDED
@@ -0,0 +1,1069 @@
1
+ # SenroUsecaser
2
+
3
+ A UseCase pattern implementation library for Ruby. Framework-agnostic with a focus on simplicity and type safety.
4
+
5
+ ## Design Philosophy
6
+
7
+ ### Single Responsibility Principle
8
+
9
+ Each UseCase handles exactly one business operation. This makes the code easier to understand, test, and maintain.
10
+
11
+ ```ruby
12
+ # Good: Single responsibility
13
+ class CreateUserUseCase < SenroUsecaser::Base
14
+ def call(input)
15
+ # Only user creation
16
+ end
17
+ end
18
+
19
+ # Bad: Multiple responsibilities
20
+ class CreateUserAndSendEmailUseCase < SenroUsecaser::Base
21
+ def call(input)
22
+ # User creation + email sending (should be separated)
23
+ end
24
+ end
25
+ ```
26
+
27
+ ### Result Pattern
28
+
29
+ All UseCases explicitly return success or failure. Instead of relying on exceptions, callers can handle results appropriately.
30
+
31
+ ```ruby
32
+ result = CreateUserUseCase.call(CreateUserUseCase::Input.new(name: "Taro", email: "taro@example.com"))
33
+
34
+ if result.success?
35
+ user = result.value
36
+ # Handle success
37
+ else
38
+ errors = result.errors
39
+ # Handle failure
40
+ end
41
+ ```
42
+
43
+ ### Dependency Injection via DI Container
44
+
45
+ UseCase dependencies are injected through a DI Container. This enables easy mock substitution during testing and achieves loose coupling.
46
+
47
+ ```ruby
48
+ class CreateUserUseCase < SenroUsecaser::Base
49
+ depends_on :user_repository, UserRepository
50
+ depends_on :event_publisher, EventPublisher
51
+
52
+ class Input
53
+ #: (name: String, email: String, **untyped) -> void
54
+ def initialize(name:, email:, **_rest)
55
+ @name = name
56
+ @email = email
57
+ end
58
+
59
+ def name = @name
60
+ def email = @email
61
+ end
62
+
63
+ input Input
64
+ output User
65
+
66
+ def call(input)
67
+ user = user_repository.create(name: input.name, email: input.email)
68
+ event_publisher.publish(UserCreated.new(user))
69
+ success(user)
70
+ end
71
+ end
72
+
73
+ # In tests
74
+ container = SenroUsecaser::Container.new
75
+ container.register(:user_repository, MockUserRepository.new)
76
+ container.register(:event_publisher, MockEventPublisher.new)
77
+ ```
78
+
79
+ #### Namespaces
80
+
81
+ The DI Container and UseCases support hierarchical namespaces for organizing dependencies and controlling visibility.
82
+
83
+ ##### Namespace Basics
84
+
85
+ ```ruby
86
+ container = SenroUsecaser::Container.new
87
+
88
+ # Register in root namespace (global)
89
+ container.register(:logger, Logger.new)
90
+ container.register(:config, AppConfig.new)
91
+
92
+ # Register in nested namespaces
93
+ container.namespace(:admin) do
94
+ register(:user_repository, AdminUserRepository.new)
95
+ register(:audit_logger, AuditLogger.new)
96
+
97
+ namespace(:reports) do
98
+ register(:report_generator, ReportGenerator.new)
99
+ register(:export_service, ExportService.new)
100
+ end
101
+ end
102
+
103
+ container.namespace(:public) do
104
+ register(:user_repository, PublicUserRepository.new)
105
+ end
106
+ ```
107
+
108
+ ##### Namespace Resolution Rules
109
+
110
+ Dependencies are resolved by looking up the current namespace and its ancestors (parents). Child namespaces are not accessible.
111
+
112
+ ```
113
+ root
114
+ ├── :logger ← accessible from anywhere
115
+ ├── :config ← accessible from anywhere
116
+ ├── admin
117
+ │ ├── :user_repository ← accessible from admin and admin::*
118
+ │ ├── :audit_logger ← accessible from admin and admin::*
119
+ │ └── reports
120
+ │ ├── :report_generator ← accessible only from admin::reports
121
+ │ └── :export_service ← accessible only from admin::reports
122
+ └── public
123
+ └── :user_repository ← accessible only from public and public::*
124
+ ```
125
+
126
+ ##### UseCase Namespace Declaration
127
+
128
+ ```ruby
129
+ # UseCase in root namespace (default)
130
+ class CreateUserUseCase < SenroUsecaser::Base
131
+ depends_on :logger # resolves from root
132
+
133
+ def call(input); end
134
+ end
135
+
136
+ # UseCase in admin namespace
137
+ class Admin::CreateUserUseCase < SenroUsecaser::Base
138
+ namespace :admin
139
+
140
+ depends_on :user_repository # resolves from admin
141
+ depends_on :audit_logger # resolves from admin
142
+ depends_on :logger # resolves from root (inherited)
143
+
144
+ def call(input); end
145
+ end
146
+
147
+ # UseCase in admin::reports namespace
148
+ class Admin::Reports::GenerateReportUseCase < SenroUsecaser::Base
149
+ namespace "admin::reports"
150
+
151
+ depends_on :report_generator # resolves from admin::reports
152
+ depends_on :user_repository # resolves from admin (parent)
153
+ depends_on :logger # resolves from root (ancestor)
154
+
155
+ def call(input); end
156
+ end
157
+ ```
158
+
159
+ ##### Automatic Namespace Inference
160
+
161
+ Instead of explicitly declaring `namespace`, you can enable automatic inference from the Ruby module structure:
162
+
163
+ ```ruby
164
+ SenroUsecaser.configure do |config|
165
+ config.infer_namespace_from_module = true
166
+ end
167
+ ```
168
+
169
+ With this enabled, namespaces are automatically derived from module names:
170
+
171
+ ```ruby
172
+ # No explicit namespace declaration needed!
173
+
174
+ # Module "Admin" → namespace "admin"
175
+ module Admin
176
+ class CreateUserUseCase < SenroUsecaser::Base
177
+ depends_on :user_repository # resolves from admin namespace
178
+ def call(input); end
179
+ end
180
+ end
181
+
182
+ # Module "Admin::Reports" → namespace "admin::reports"
183
+ module Admin
184
+ module Reports
185
+ class GenerateReportUseCase < SenroUsecaser::Base
186
+ depends_on :report_generator # resolves from admin::reports
187
+ depends_on :user_repository # resolves from admin (parent)
188
+ def call(input); end
189
+ end
190
+ end
191
+ end
192
+
193
+ # Top-level class → no namespace (root)
194
+ class CreateUserUseCase < SenroUsecaser::Base
195
+ depends_on :logger # resolves from root
196
+ def call(input); end
197
+ end
198
+ ```
199
+
200
+ This also works for Providers:
201
+
202
+ ```ruby
203
+ module Admin
204
+ class ServiceProvider < SenroUsecaser::Provider
205
+ # Automatically registers in "admin" namespace
206
+ def register(container)
207
+ container.register(:admin_service, AdminService.new)
208
+ end
209
+ end
210
+ end
211
+ ```
212
+
213
+ **Note:** Explicit `namespace` declarations take precedence over inferred namespaces.
214
+
215
+ ##### Scoped Containers
216
+
217
+ Create child containers for request-scoped dependencies (e.g., current_user):
218
+
219
+ ```ruby
220
+ # Global container with lazy registration
221
+ SenroUsecaser.container.register_lazy(:task_repository) do |c|
222
+ TaskRepository.new(current_user: c.resolve(:current_user))
223
+ end
224
+
225
+ # Per-request: create scoped container with current_user
226
+ request_container = SenroUsecaser.container.scope do
227
+ register(:current_user, current_user)
228
+ end
229
+
230
+ # UseCase resolves task_repository with correct current_user
231
+ ListTasksUseCase.call(input, container: request_container)
232
+ ```
233
+
234
+ #### Providers (Multi-file Registration)
235
+
236
+ For large applications, dependencies can be organized into Provider classes across multiple files. Providers declare their dependencies on other providers, ensuring correct load order.
237
+
238
+ ##### Defining Providers
239
+
240
+ ```ruby
241
+ # app/providers/core_provider.rb
242
+ class CoreProvider < SenroUsecaser::Provider
243
+ def register(container)
244
+ container.register(:logger, Logger.new(STDOUT))
245
+ container.register(:config, AppConfig.load)
246
+ end
247
+ end
248
+
249
+ # app/providers/persistence_provider.rb
250
+ class PersistenceProvider < SenroUsecaser::Provider
251
+ depends_on CoreProvider # Ensures CoreProvider loads first
252
+
253
+ def register(container)
254
+ container.register_singleton(:database) do |c|
255
+ Database.connect(c.resolve(:config))
256
+ end
257
+ end
258
+ end
259
+
260
+ # app/providers/admin_provider.rb
261
+ class AdminProvider < SenroUsecaser::Provider
262
+ depends_on CoreProvider
263
+ depends_on PersistenceProvider
264
+
265
+ namespace :admin # Register in admin namespace
266
+
267
+ def register(container)
268
+ container.register(:user_repository, AdminUserRepository.new)
269
+ end
270
+ end
271
+ ```
272
+
273
+ ##### Booting the Container
274
+
275
+ ```ruby
276
+ # config/initializers/senro_usecaser.rb
277
+ SenroUsecaser.configure do |config|
278
+ config.providers = [
279
+ CoreProvider,
280
+ PersistenceProvider,
281
+ AdminProvider
282
+ ]
283
+ end
284
+
285
+ # Boot resolves dependencies and loads in correct order:
286
+ # 1. CoreProvider (no dependencies)
287
+ # 2. PersistenceProvider (depends on Core)
288
+ # 3. AdminProvider (depends on Core, Persistence)
289
+ SenroUsecaser.boot!
290
+ ```
291
+
292
+ ##### Provider Lifecycle Hooks
293
+
294
+ ```ruby
295
+ class PersistenceProvider < SenroUsecaser::Provider
296
+ depends_on CoreProvider
297
+
298
+ # Called before register
299
+ def before_register(container)
300
+ # Setup work
301
+ end
302
+
303
+ def register(container)
304
+ container.register_singleton(:database) do |c|
305
+ Database.connect(c.resolve(:config))
306
+ end
307
+ end
308
+
309
+ # Called after all providers are registered
310
+ def after_boot(container)
311
+ container.resolve(:database).verify_connection!
312
+ end
313
+
314
+ # Called on application shutdown
315
+ def shutdown(container)
316
+ container.resolve(:database).disconnect
317
+ end
318
+ end
319
+ ```
320
+
321
+ ##### Registration Types
322
+
323
+ ```ruby
324
+ class PersistenceProvider < SenroUsecaser::Provider
325
+ def register(container)
326
+ # Eager: value stored directly
327
+ container.register(:config, AppConfig.load)
328
+
329
+ # Lazy: block called on every resolve
330
+ container.register_lazy(:connection) do |c|
331
+ Database.connect(c.resolve(:config))
332
+ end
333
+
334
+ # Singleton: block called once, result cached
335
+ container.register_singleton(:connection_pool) do |c|
336
+ ConnectionPool.new(size: 10) { c.resolve(:connection) }
337
+ end
338
+ end
339
+ end
340
+ ```
341
+
342
+ ##### Conditional Providers
343
+
344
+ ```ruby
345
+ class DevelopmentProvider < SenroUsecaser::Provider
346
+ enabled_if { SenroUsecaser.env.development? }
347
+
348
+ def register(container)
349
+ container.register(:mailer, DevelopmentMailer.new)
350
+ end
351
+ end
352
+
353
+ class ProductionProvider < SenroUsecaser::Provider
354
+ enabled_if { SenroUsecaser.env.production? }
355
+
356
+ def register(container)
357
+ container.register(:mailer, SmtpMailer.new)
358
+ end
359
+ end
360
+ ```
361
+
362
+ ##### Provider Dependency Graph
363
+
364
+ The container ensures providers are loaded in topological order based on dependencies. Circular dependencies are detected and raise an error at boot time.
365
+
366
+ ### Testability
367
+
368
+ Dependency injection allows unit testing UseCases without relying on external services or databases.
369
+
370
+ ```ruby
371
+ RSpec.describe CreateUserUseCase do
372
+ let(:user_repository) { instance_double(UserRepository) }
373
+ let(:use_case) { described_class.new(dependencies: { user_repository: user_repository }) }
374
+
375
+ it "creates a user" do
376
+ allow(user_repository).to receive(:create).and_return(user)
377
+
378
+ input = CreateUserUseCase::Input.new(name: "Taro", email: "taro@example.com")
379
+ result = use_case.call(input)
380
+
381
+ expect(result).to be_success
382
+ end
383
+ end
384
+ ```
385
+
386
+ ### Framework Agnostic
387
+
388
+ Implemented in pure Ruby, it can be used with any framework such as Rails, Sinatra, or Hanami.
389
+
390
+ ### Type Safety (RBS Inline)
391
+
392
+ All implementations are designed to be type-safe using **RBS Inline** comments. Types are written directly in Ruby source files as comments.
393
+
394
+ #### Input/Output Classes
395
+
396
+ Each UseCase defines its Input and Output as inner classes with RBS Inline annotations:
397
+
398
+ ```ruby
399
+ class CreateUserUseCase < SenroUsecaser::Base
400
+ class Input
401
+ #: (name: String, email: String, ?age: Integer, **untyped) -> void
402
+ def initialize(name:, email:, age: nil, **_rest)
403
+ @name = name #: String
404
+ @email = email #: String
405
+ @age = age #: Integer?
406
+ end
407
+
408
+ #: () -> String
409
+ def name = @name
410
+
411
+ #: () -> String
412
+ def email = @email
413
+
414
+ #: () -> Integer?
415
+ def age = @age
416
+ end
417
+
418
+ class Output
419
+ #: (user: User, token: String) -> void
420
+ def initialize(user:, token:)
421
+ @user = user #: User
422
+ @token = token #: String
423
+ end
424
+
425
+ #: () -> User
426
+ def user = @user
427
+
428
+ #: () -> String
429
+ def token = @token
430
+ end
431
+
432
+ input Input
433
+ output Output
434
+
435
+ def call(input)
436
+ user = User.create(name: input.name, email: input.email, age: input.age)
437
+ token = generate_token(user)
438
+ success(Output.new(user: user, token: token))
439
+ end
440
+ end
441
+ ```
442
+
443
+ The `**_rest` parameter in Input's initialize allows extra fields to be passed through pipeline steps without errors.
444
+
445
+ ### Simplicity
446
+
447
+ Define UseCases with minimal boilerplate. Avoids over-abstraction and provides an intuitive API.
448
+
449
+ ### UseCase Composition
450
+
451
+ Complex business operations can be composed from simpler UseCases using `organize` and `extend_with`.
452
+
453
+ #### organize - Sequential Execution
454
+
455
+ Execute multiple UseCases in sequence. Each step's output object becomes the next step's input directly (type chaining).
456
+
457
+ **Important:** All pipeline steps must define an `input` class. The output of step A should be compatible with the input of step B.
458
+
459
+ ```ruby
460
+ class PlaceOrderUseCase < SenroUsecaser::Base
461
+ class Input
462
+ #: (user_id: Integer, product_ids: Array[Integer], **untyped) -> void
463
+ def initialize(user_id:, product_ids:, **_rest)
464
+ @user_id = user_id
465
+ @product_ids = product_ids
466
+ end
467
+
468
+ def user_id = @user_id
469
+ def product_ids = @product_ids
470
+ end
471
+
472
+ input Input
473
+ output CreateOrderOutput
474
+
475
+ # Each step's output becomes the next step's input:
476
+ # PlaceOrderUseCase::Input -> ValidateOrderUseCase
477
+ # ValidateOrderUseCase::Output -> CreateOrderUseCase::Input
478
+ # CreateOrderUseCase::Output -> ChargePaymentUseCase::Input
479
+ # ChargePaymentUseCase::Output -> SendConfirmationEmailUseCase::Input
480
+ # SendConfirmationEmailUseCase::Output -> CreateOrderOutput
481
+ organize do
482
+ step ValidateOrderUseCase
483
+ step CreateOrderUseCase
484
+ step ChargePaymentUseCase
485
+ step SendConfirmationEmailUseCase
486
+ end
487
+ end
488
+ ```
489
+
490
+ ##### Error Handling Strategy
491
+
492
+ Configure how errors are handled using the `on_failure:` option.
493
+
494
+ **`:stop` (default)** - Stop execution on first failure.
495
+
496
+ ```ruby
497
+ class PlaceOrderUseCase < SenroUsecaser::Base
498
+ organize on_failure: :stop do
499
+ step ValidateOrderUseCase
500
+ step CreateOrderUseCase
501
+ step ChargePaymentUseCase # Not executed if CreateOrderUseCase fails
502
+ end
503
+ end
504
+ ```
505
+
506
+ **`:continue`** - Continue execution even if a step fails.
507
+
508
+ ```ruby
509
+ class BatchProcessUseCase < SenroUsecaser::Base
510
+ organize on_failure: :continue do
511
+ step ProcessItemAUseCase
512
+ step ProcessItemBUseCase # Executed even if A fails
513
+ step ProcessItemCUseCase
514
+ end
515
+ end
516
+ ```
517
+
518
+ **`:collect`** - Continue execution and collect all errors.
519
+
520
+ ```ruby
521
+ class ValidateFormUseCase < SenroUsecaser::Base
522
+ organize on_failure: :collect do
523
+ step ValidateNameUseCase
524
+ step ValidateEmailUseCase
525
+ step ValidatePasswordUseCase
526
+ end
527
+ end
528
+
529
+ result = ValidateFormUseCase.call(input)
530
+ result.errors # => [name_error, email_error, password_error]
531
+ ```
532
+
533
+ ##### Per-Step Error Handling
534
+
535
+ ```ruby
536
+ class PlaceOrderUseCase < SenroUsecaser::Base
537
+ organize do
538
+ step ValidateOrderUseCase
539
+ step CreateOrderUseCase
540
+ step SendConfirmationEmailUseCase, on_failure: :continue # Don't fail if email fails
541
+ step NotifyAnalyticsUseCase, on_failure: :continue # Optional step
542
+ end
543
+ end
544
+ ```
545
+
546
+ #### Conditional Execution with `if:` / `unless:`
547
+
548
+ Use `if:` or `unless:` options to conditionally execute steps.
549
+
550
+ ```ruby
551
+ class PlaceOrderUseCase < SenroUsecaser::Base
552
+ organize do
553
+ step ValidateOrderUseCase
554
+ step ApplyCouponUseCase, if: :has_coupon?
555
+ step CreateOrderUseCase
556
+ step ChargePaymentUseCase, unless: :free_order?
557
+ step SendGiftNotificationUseCase, if: :gift_order?
558
+ end
559
+
560
+ private
561
+
562
+ # Condition methods receive the current input object (output from previous step)
563
+ def has_coupon?(input)
564
+ input.coupon_code.present?
565
+ end
566
+
567
+ def free_order?(input)
568
+ input.total.zero?
569
+ end
570
+
571
+ def gift_order?(input)
572
+ input.gift_recipient.present?
573
+ end
574
+ end
575
+ ```
576
+
577
+ ##### Condition as Lambda
578
+
579
+ ```ruby
580
+ class PlaceOrderUseCase < SenroUsecaser::Base
581
+ organize do
582
+ step ValidateOrderUseCase
583
+ step ApplyCouponUseCase, if: ->(input) { input.coupon_code.present? }
584
+ step NotifyAdminUseCase, if: ->(input) { input.total > 10_000 }
585
+ end
586
+ end
587
+ ```
588
+
589
+ ##### Multiple Conditions
590
+
591
+ Combine multiple conditions with `all:` (AND) or `any:` (OR):
592
+
593
+ ```ruby
594
+ class PlaceOrderUseCase < SenroUsecaser::Base
595
+ organize do
596
+ step ValidateOrderUseCase
597
+ # Runs only if ALL conditions are true
598
+ step PremiumDiscountUseCase, all: [:premium_user?, :eligible_for_discount?]
599
+ # Runs if ANY condition is true
600
+ step SendNotificationUseCase, any: [:email_opted_in?, :sms_opted_in?]
601
+ end
602
+ end
603
+ ```
604
+
605
+ #### Custom Input Mapping
606
+
607
+ By default, the previous step's output object is passed directly as input to the next step. Use `input:` to transform it:
608
+
609
+ ```ruby
610
+ class PlaceOrderUseCase < SenroUsecaser::Base
611
+ organize do
612
+ step ValidateOrderUseCase
613
+ step CreateOrderUseCase
614
+
615
+ # Method reference - transform current input for next step
616
+ step SendEmailUseCase, input: :prepare_email_input
617
+
618
+ # Lambda - transform current input
619
+ step NotifyUseCase, input: ->(input) { NotifyInput.new(message: "Order #{input.order_id}") }
620
+ end
621
+
622
+ def prepare_email_input(input)
623
+ SendEmailInput.new(to: input.customer_email, subject: "Order Confirmation")
624
+ end
625
+ end
626
+ ```
627
+
628
+ #### extend_with - Hooks (before/after/around)
629
+
630
+ Add cross-cutting concerns like logging, authorization, or transaction handling.
631
+
632
+ ```ruby
633
+ # Define extension modules
634
+ module Logging
635
+ def self.before(input)
636
+ puts "Starting: #{input.class.name}"
637
+ end
638
+
639
+ def self.after(input, result)
640
+ puts "Finished: #{result.success? ? 'success' : 'failure'}"
641
+ end
642
+ end
643
+
644
+ module Transaction
645
+ def self.around(input, &block)
646
+ ActiveRecord::Base.transaction { block.call }
647
+ end
648
+ end
649
+
650
+ # Apply to UseCase
651
+ class CreateUserUseCase < SenroUsecaser::Base
652
+ extend_with Logging, Transaction
653
+
654
+ def call(input)
655
+ # main logic
656
+ end
657
+ end
658
+ ```
659
+
660
+ ##### Block Syntax
661
+
662
+ ```ruby
663
+ class CreateUserUseCase < SenroUsecaser::Base
664
+ before do |input|
665
+ # runs before call
666
+ end
667
+
668
+ after do |input, result|
669
+ # runs after call
670
+ end
671
+
672
+ around do |input, &block|
673
+ ActiveRecord::Base.transaction do
674
+ block.call
675
+ end
676
+ end
677
+
678
+ def call(input)
679
+ # main logic
680
+ end
681
+ end
682
+ ```
683
+
684
+ ##### Input/Output Validation
685
+
686
+ Use `extend_with` to integrate validation libraries like ActiveModel::Validations:
687
+
688
+ ```ruby
689
+ # Define validation extension
690
+ module InputValidation
691
+ def self.around(input, &block)
692
+ # input is the input object passed to call
693
+ return block.call unless input.respond_to?(:validate!)
694
+
695
+ input.validate!
696
+ block.call
697
+ rescue ActiveModel::ValidationError => e
698
+ errors = e.model.errors.map do |error|
699
+ SenroUsecaser::Error.new(
700
+ code: :validation_error,
701
+ field: error.attribute,
702
+ message: error.full_message
703
+ )
704
+ end
705
+ SenroUsecaser::Result.failure(*errors)
706
+ end
707
+ end
708
+
709
+ module OutputValidation
710
+ def self.after(input, result)
711
+ return unless result.success?
712
+
713
+ output = result.value
714
+ output.validate! if output.respond_to?(:validate!)
715
+ rescue ActiveModel::ValidationError => e
716
+ Rails.logger.error("Output validation failed: #{e.message}")
717
+ end
718
+ end
719
+
720
+ # Input class with ActiveModel validations
721
+ class CreateUserInput
722
+ include ActiveModel::Validations
723
+
724
+ attr_accessor :name, :email
725
+
726
+ validates :name, presence: true, length: { minimum: 2 }
727
+ validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
728
+
729
+ def initialize(name:, email:)
730
+ @name = name
731
+ @email = email
732
+ end
733
+ end
734
+
735
+ # Apply validation to UseCase using input class declaration
736
+ class CreateUserUseCase < SenroUsecaser::Base
737
+ input CreateUserInput
738
+ extend_with InputValidation, OutputValidation
739
+
740
+ def call(user_input)
741
+ # user_input is already validated by InputValidation hook
742
+ User.create!(name: user_input.name, email: user_input.email)
743
+ end
744
+ end
745
+
746
+ # Usage - pass input object directly
747
+ input = CreateUserInput.new(name: "", email: "invalid")
748
+ result = CreateUserUseCase.call(input)
749
+ result.failure? # => true
750
+ result.errors.first.field # => :name
751
+ ```
752
+
753
+ #### Combining Composition Patterns
754
+
755
+ ```ruby
756
+ class RegisterUserUseCase < SenroUsecaser::Base
757
+ class Input
758
+ #: (name: String, email: String, password: String, **untyped) -> void
759
+ def initialize(name:, email:, password:, **_rest)
760
+ @name = name
761
+ @email = email
762
+ @password = password
763
+ end
764
+
765
+ def name = @name
766
+ def email = @email
767
+ def password = @password
768
+ end
769
+
770
+ # Hooks
771
+ extend_with Logging
772
+ extend_with TransactionWrapper
773
+
774
+ input Input
775
+ output UserOutput
776
+
777
+ # Pipeline
778
+ organize do
779
+ step ValidateUserInputUseCase
780
+ step CheckDuplicateEmailUseCase
781
+ step CreateUserUseCase
782
+ step SendWelcomeEmailUseCase, on_failure: :continue
783
+ end
784
+ end
785
+ ```
786
+
787
+ ## Installation
788
+
789
+ ```bash
790
+ bundle add senro_usecaser
791
+ ```
792
+
793
+ Or add to your Gemfile:
794
+
795
+ ```ruby
796
+ gem "senro_usecaser"
797
+ ```
798
+
799
+ ## Usage
800
+
801
+ ### Basic UseCase
802
+
803
+ ```ruby
804
+ class CreateUserUseCase < SenroUsecaser::Base
805
+ class Input
806
+ #: (name: String, email: String, **untyped) -> void
807
+ def initialize(name:, email:, **_rest)
808
+ @name = name #: String
809
+ @email = email #: String
810
+ end
811
+
812
+ #: () -> String
813
+ def name = @name
814
+
815
+ #: () -> String
816
+ def email = @email
817
+ end
818
+
819
+ class Output
820
+ #: (user: User) -> void
821
+ def initialize(user:)
822
+ @user = user #: User
823
+ end
824
+
825
+ #: () -> User
826
+ def user = @user
827
+ end
828
+
829
+ input Input
830
+ output Output
831
+
832
+ def call(input)
833
+ user = User.create(name: input.name, email: input.email)
834
+ success(Output.new(user: user))
835
+ rescue ActiveRecord::RecordInvalid => e
836
+ failure(SenroUsecaser::Error.new(
837
+ code: :validation_error,
838
+ message: e.message
839
+ ))
840
+ end
841
+ end
842
+
843
+ input = CreateUserUseCase::Input.new(name: "Taro", email: "taro@example.com")
844
+ result = CreateUserUseCase.call(input)
845
+
846
+ if result.success?
847
+ puts "Created user: #{result.value.user.name}"
848
+ else
849
+ puts "Error: #{result.errors.first.message}"
850
+ end
851
+ ```
852
+
853
+ ### With Dependencies
854
+
855
+ ```ruby
856
+ class CreateUserUseCase < SenroUsecaser::Base
857
+ depends_on :user_repository, UserRepository
858
+ depends_on :event_publisher, EventPublisher
859
+
860
+ class Input
861
+ #: (name: String, email: String, **untyped) -> void
862
+ def initialize(name:, email:, **_rest)
863
+ @name = name
864
+ @email = email
865
+ end
866
+
867
+ def name = @name
868
+ def email = @email
869
+ end
870
+
871
+ class Output
872
+ #: (user: User) -> void
873
+ def initialize(user:)
874
+ @user = user
875
+ end
876
+
877
+ def user = @user
878
+ end
879
+
880
+ input Input
881
+ output Output
882
+
883
+ def call(input)
884
+ user = user_repository.create(name: input.name, email: input.email)
885
+ event_publisher.publish(UserCreated.new(user))
886
+ success(Output.new(user: user))
887
+ end
888
+ end
889
+
890
+ # Register dependencies
891
+ SenroUsecaser.container.register(:user_repository, UserRepository.new)
892
+ SenroUsecaser.container.register(:event_publisher, EventPublisher.new)
893
+
894
+ # Call
895
+ input = CreateUserUseCase::Input.new(name: "Taro", email: "taro@example.com")
896
+ result = CreateUserUseCase.call(input)
897
+ ```
898
+
899
+ ### Calling UseCases
900
+
901
+ #### `.call` vs `.call!`
902
+
903
+ SenroUsecaser provides two methods for invoking a UseCase:
904
+
905
+ **`.call`** - Standard invocation. Exceptions are not automatically caught.
906
+
907
+ ```ruby
908
+ result = CreateUserUseCase.call(input)
909
+ # If an unhandled exception is raised, it propagates up
910
+ ```
911
+
912
+ **`.call!`** - Safe invocation. Any `StandardError` is caught and converted to `Result.failure`.
913
+
914
+ ```ruby
915
+ result = CreateUserUseCase.call!(input)
916
+ # If User.create raises an exception, result is:
917
+ # Result.failure(Error.new(code: :exception, message: "...", cause: exception))
918
+
919
+ if result.failure?
920
+ error = result.errors.first
921
+ error.code # => :exception
922
+ error.message # => Exception message
923
+ error.cause # => Original exception object
924
+ end
925
+ ```
926
+
927
+ Use `.call!` when you want to ensure all exceptions are captured as `Result.failure` without explicit rescue blocks in your UseCase.
928
+
929
+ #### Exception Handling in Pipelines
930
+
931
+ When using `.call!` with `organize` pipelines, the exception capture behavior is **chained** to all steps. This is especially useful with `on_failure: :collect`:
932
+
933
+ ```ruby
934
+ class PlaceOrderUseCase < SenroUsecaser::Base
935
+ organize on_failure: :collect do
936
+ step ValidateOrderUseCase # Raises exception -> captured as Result.failure
937
+ step ChargePaymentUseCase # Raises exception -> captured as Result.failure
938
+ step SendEmailUseCase # Returns explicit failure
939
+ end
940
+ end
941
+
942
+ result = PlaceOrderUseCase.call!(input)
943
+ # All errors (from exceptions and explicit failures) are collected
944
+ result.errors # => [exception_error_1, exception_error_2, explicit_error]
945
+ ```
946
+
947
+ **Behavior comparison:**
948
+
949
+ | Call method | Pipeline step behavior | Exception handling |
950
+ |-------------|----------------------|-------------------|
951
+ | `.call` | Steps use `.call` | Exception propagates up |
952
+ | `.call!` | Steps use `.call!` | Exception → `Result.failure`, collected if `:collect` |
953
+
954
+ This chaining also applies to nested pipelines:
955
+
956
+ ```ruby
957
+ class InnerUseCase < SenroUsecaser::Base
958
+ organize on_failure: :collect do
959
+ step StepA # Raises exception
960
+ end
961
+ end
962
+
963
+ class OuterUseCase < SenroUsecaser::Base
964
+ organize on_failure: :collect do
965
+ step InnerUseCase # Inner exception is captured
966
+ step StepB # Raises exception
967
+ end
968
+ end
969
+
970
+ result = OuterUseCase.call!(input)
971
+ result.errors # => [inner_exception_error, step_b_exception_error]
972
+ ```
973
+
974
+ #### Implicit Success Wrapping
975
+
976
+ By default, if a `call` method returns a non-Result value, it is automatically wrapped in `Result.success`. This allows for more concise UseCase implementations.
977
+
978
+ ```ruby
979
+ # Explicit success (traditional style)
980
+ class CreateUserUseCase < SenroUsecaser::Base
981
+ def call(input)
982
+ user = User.create(name: input.name)
983
+ success(user) # Explicitly wrap in Result.success
984
+ end
985
+ end
986
+
987
+ # Implicit success (concise style)
988
+ class CreateUserUseCase < SenroUsecaser::Base
989
+ def call(input)
990
+ User.create(name: input.name) # Automatically wrapped as Result.success(user)
991
+ end
992
+ end
993
+ ```
994
+
995
+ This works with any return type:
996
+
997
+ ```ruby
998
+ class GetUserUseCase < SenroUsecaser::Base
999
+ def call(id:)
1000
+ User.find(id) # Returns Result.success(user)
1001
+ end
1002
+ end
1003
+
1004
+ class ListUsersUseCase < SenroUsecaser::Base
1005
+ def call(**_args)
1006
+ User.all.to_a # Returns Result.success([user1, user2, ...])
1007
+ end
1008
+ end
1009
+
1010
+ class CheckHealthUseCase < SenroUsecaser::Base
1011
+ def call(**_args)
1012
+ nil # Returns Result.success(nil)
1013
+ end
1014
+ end
1015
+ ```
1016
+
1017
+ **Note:** Explicit `failure(...)` calls are never wrapped - they remain as `Result.failure`.
1018
+
1019
+ ```ruby
1020
+ class CreateUserUseCase < SenroUsecaser::Base
1021
+ def call(input)
1022
+ return failure(Error.new(code: :invalid, message: "Name required")) if input.name.empty?
1023
+
1024
+ User.create(name: input.name) # Implicit success
1025
+ end
1026
+ end
1027
+ ```
1028
+
1029
+ ### Result Operations
1030
+
1031
+ ```ruby
1032
+ input = CreateUserUseCase::Input.new(name: "Taro", email: "taro@example.com")
1033
+ result = CreateUserUseCase.call(input)
1034
+
1035
+ # Check status
1036
+ result.success? # => true/false
1037
+ result.failure? # => true/false
1038
+
1039
+ # Get value
1040
+ result.value # => Output or nil
1041
+ result.value! # => Output or raises error
1042
+ result.value_or(default) # => Output or default
1043
+
1044
+ # Transform
1045
+ result.map { |output| output.user.name } # => Result[String]
1046
+ result.and_then { |output| UpdateProfileUseCase.call(user: output.user) } # => Result[...]
1047
+
1048
+ # Handle errors
1049
+ result.errors # => Array[Error]
1050
+ result.or_else { |errors| handle_errors(errors) }
1051
+ ```
1052
+
1053
+ ## Development
1054
+
1055
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
1056
+
1057
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
1058
+
1059
+ ## Contributing
1060
+
1061
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/senro_usecaser.
1062
+
1063
+ ## Roadmap
1064
+
1065
+ The following features are planned for future releases:
1066
+
1067
+ - **Parallel execution in organize** - Execute multiple steps concurrently within a pipeline for improved performance
1068
+ - **Ruby LSP extension for Container** - IDE autocompletion support for dependency injection with Container
1069
+ - **Automatic RBS generation** - Auto-generate RBS type definitions for `input`, `output`, `call`, and `depends_on` declarations