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.
@@ -0,0 +1,1279 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # rubocop:disable all
5
+
6
+ # rbs_inline: enabled
7
+
8
+ require_relative "../lib/senro_usecaser"
9
+
10
+ # =============================================================================
11
+ # サンプル: ECサイトの注文システム
12
+ # =============================================================================
13
+ #
14
+ # このサンプルでは以下の機能を検証します:
15
+ # 1. 基本的な UseCase と Result
16
+ # 2. DI Container(依存性注入)
17
+ # 3. Provider パターン(依存関係、ライフサイクル、enabled_if)
18
+ # 4. Pipeline composition(organize, step, 条件分岐)
19
+ # 5. Hooks(before/after/around)
20
+ # 6. Scoped Container(リクエストスコープ)
21
+ # 7. Input/Output スキーマ
22
+ # 8. Accumulated Context
23
+ #
24
+ # =============================================================================
25
+
26
+ puts "=" * 70
27
+ puts "SenroUsecaser サンプルプログラム: ECサイト注文システム"
28
+ puts "=" * 70
29
+ puts
30
+
31
+ # =============================================================================
32
+ # モデル
33
+ # =============================================================================
34
+
35
+ # ユーザーモデル
36
+ class User
37
+ #: Integer
38
+ attr_reader :id
39
+
40
+ #: String
41
+ attr_reader :name
42
+
43
+ #: String
44
+ attr_reader :email
45
+
46
+ #: bool
47
+ attr_reader :premium
48
+
49
+ #: (id: Integer, name: String, email: String, premium: bool) -> void
50
+ def initialize(id:, name:, email:, premium:)
51
+ @id = id
52
+ @name = name
53
+ @email = email
54
+ @premium = premium
55
+ end
56
+
57
+ #: () -> bool
58
+ def premium?
59
+ @premium
60
+ end
61
+ end
62
+
63
+ # 商品モデル
64
+ class Product
65
+ #: Integer
66
+ attr_reader :id
67
+
68
+ #: String
69
+ attr_reader :name
70
+
71
+ #: Integer
72
+ attr_reader :price
73
+
74
+ #: Integer
75
+ attr_reader :stock
76
+
77
+ #: (id: Integer, name: String, price: Integer, stock: Integer) -> void
78
+ def initialize(id:, name:, price:, stock:)
79
+ @id = id
80
+ @name = name
81
+ @price = price
82
+ @stock = stock
83
+ end
84
+ end
85
+
86
+ # 注文モデル
87
+ class Order
88
+ #: Integer
89
+ attr_reader :id
90
+
91
+ #: Integer
92
+ attr_reader :user_id
93
+
94
+ #: Array[Integer]
95
+ attr_reader :items
96
+
97
+ #: Integer
98
+ attr_reader :subtotal
99
+
100
+ #: Integer
101
+ attr_reader :tax
102
+
103
+ #: Integer
104
+ attr_reader :discount
105
+
106
+ #: Integer
107
+ attr_reader :total
108
+
109
+ #: String
110
+ attr_reader :status
111
+
112
+ #: (id: Integer, user_id: Integer, items: Array[Integer], subtotal: Integer, tax: Integer, discount: Integer, total: Integer, status: String) -> void
113
+ def initialize(id:, user_id:, items:, subtotal:, tax:, discount:, total:, status:)
114
+ @id = id
115
+ @user_id = user_id
116
+ @items = items
117
+ @subtotal = subtotal
118
+ @tax = tax
119
+ @discount = discount
120
+ @total = total
121
+ @status = status
122
+ end
123
+ end
124
+
125
+ # =============================================================================
126
+ # 共有 Output 型定義(複数 UseCase で使用)
127
+ # =============================================================================
128
+
129
+ class CreateOrderOutput
130
+ #: (order: Order) -> void
131
+ def initialize(order:)
132
+ @order = order #: Order
133
+ end
134
+
135
+ #: () -> Order
136
+ attr_reader :order
137
+ end
138
+
139
+ class GreetingOutput
140
+ #: (greeted: String) -> void
141
+ def initialize(greeted:)
142
+ @greeted = greeted #: String
143
+ end
144
+
145
+ #: () -> String
146
+ attr_reader :greeted
147
+ end
148
+
149
+ class AccumulatedOutput
150
+ #: (counter: Integer, final: bool) -> void
151
+ def initialize(counter:, final:)
152
+ @counter = counter #: Integer
153
+ @final = final #: bool
154
+ end
155
+
156
+ #: () -> Integer
157
+ attr_reader :counter
158
+
159
+ #: () -> bool
160
+ attr_reader :final
161
+ end
162
+
163
+ # =============================================================================
164
+ # リポジトリ(インメモリ実装)
165
+ # =============================================================================
166
+
167
+ class UserRepository
168
+ #: () -> void
169
+ def initialize
170
+ @users = {
171
+ 1 => User.new(id: 1, name: "田中太郎", email: "tanaka@example.com", premium: true),
172
+ 2 => User.new(id: 2, name: "佐藤花子", email: "sato@example.com", premium: false)
173
+ } #: Hash[Integer, User]
174
+ end
175
+
176
+ #: (Integer) -> User?
177
+ def find(id)
178
+ @users[id]
179
+ end
180
+ end
181
+
182
+ class ProductRepository
183
+ #: () -> void
184
+ def initialize
185
+ @products = {
186
+ 101 => Product.new(id: 101, name: "ノートPC", price: 120_000, stock: 5),
187
+ 102 => Product.new(id: 102, name: "マウス", price: 3_000, stock: 50),
188
+ 103 => Product.new(id: 103, name: "キーボード", price: 8_000, stock: 0)
189
+ } #: Hash[Integer, Product]
190
+ end
191
+
192
+ #: (Integer) -> Product?
193
+ def find(id)
194
+ @products[id]
195
+ end
196
+
197
+ #: (Array[Integer]) -> Array[Product]
198
+ def find_all(ids)
199
+ ids.filter_map { |id| @products[id] }
200
+ end
201
+ end
202
+
203
+ class OrderRepository
204
+ #: () -> void
205
+ def initialize
206
+ @orders = {} #: Hash[Integer, Order]
207
+ @next_id = 1 #: Integer
208
+ end
209
+
210
+ #: (user_id: Integer, items: Array[Integer], subtotal: Integer, tax: Integer, discount: Integer, total: Integer) -> Order
211
+ def create(user_id:, items:, subtotal:, tax:, discount:, total:)
212
+ order = Order.new(
213
+ id: @next_id,
214
+ user_id: user_id,
215
+ items: items,
216
+ subtotal: subtotal,
217
+ tax: tax,
218
+ discount: discount,
219
+ total: total,
220
+ status: "pending"
221
+ )
222
+ @orders[@next_id] = order
223
+ @next_id += 1
224
+ order
225
+ end
226
+ end
227
+
228
+ # =============================================================================
229
+ # サービス
230
+ # =============================================================================
231
+
232
+ # 決済結果
233
+ class PaymentResult
234
+ #: String
235
+ attr_reader :transaction_id
236
+
237
+ #: Time
238
+ attr_reader :charged_at
239
+
240
+ #: (transaction_id: String, charged_at: Time) -> void
241
+ def initialize(transaction_id:, charged_at:)
242
+ @transaction_id = transaction_id
243
+ @charged_at = charged_at
244
+ end
245
+ end
246
+
247
+ class PaymentService
248
+ #: (user: User, amount: Integer) -> PaymentResult
249
+ def charge(user:, amount:)
250
+ puts " [PaymentService] #{user.name} に ¥#{amount} を請求"
251
+ PaymentResult.new(transaction_id: "TXN-#{rand(10_000..99_999)}", charged_at: Time.now)
252
+ end
253
+ end
254
+
255
+ class NotificationService
256
+ #: (to: String, subject: String, body: String) -> bool
257
+ def send_email(to:, subject:, body:)
258
+ puts " [NotificationService] メール送信: #{to} - #{subject}"
259
+ true
260
+ end
261
+ end
262
+
263
+ class DiscountService
264
+ #: (Integer) -> Integer
265
+ def calculate_premium_discount(subtotal)
266
+ (subtotal * 0.1).round
267
+ end
268
+ end
269
+
270
+ class Logger
271
+ #: (String) -> void
272
+ def info(message)
273
+ puts " [LOG] #{message}"
274
+ end
275
+ end
276
+
277
+ # =============================================================================
278
+ # Provider 定義
279
+ # =============================================================================
280
+
281
+ # コアプロバイダー: 基本サービスを登録
282
+ class CoreProvider < SenroUsecaser::Provider
283
+ #: (SenroUsecaser::Container) -> void
284
+ def register(container)
285
+ container.register(:logger, Logger.new)
286
+ end
287
+ end
288
+
289
+ # インフラプロバイダー: リポジトリを登録
290
+ class InfrastructureProvider < SenroUsecaser::Provider
291
+ depends_on CoreProvider
292
+
293
+ #: (SenroUsecaser::Container) -> void
294
+ def register(container)
295
+ container.register_singleton(:user_repository) { |_c| UserRepository.new }
296
+ container.register_singleton(:product_repository) { |_c| ProductRepository.new }
297
+ container.register_singleton(:order_repository) { |_c| OrderRepository.new }
298
+ end
299
+
300
+ #: (SenroUsecaser::Container) -> void
301
+ def after_boot(container)
302
+ logger = container.resolve(:logger) #: Logger
303
+ logger.info("InfrastructureProvider: リポジトリ初期化完了")
304
+ end
305
+ end
306
+
307
+ # サービスプロバイダー: ビジネスサービスを登録
308
+ class ServiceProvider < SenroUsecaser::Provider
309
+ depends_on CoreProvider
310
+
311
+ #: (SenroUsecaser::Container) -> void
312
+ def register(container)
313
+ container.register_singleton(:payment_service) { |_c| PaymentService.new }
314
+ container.register_singleton(:notification_service) { |_c| NotificationService.new }
315
+ container.register_singleton(:discount_service) { |_c| DiscountService.new }
316
+ end
317
+ end
318
+
319
+ # 開発環境用プロバイダー: 開発環境でのみ有効
320
+ class DevelopmentProvider < SenroUsecaser::Provider
321
+ enabled_if { SenroUsecaser.env.development? }
322
+
323
+ #: (SenroUsecaser::Container) -> void
324
+ def register(container)
325
+ container.register(:debug_mode, true)
326
+ end
327
+ end
328
+
329
+ # =============================================================================
330
+ # 個別 UseCase(パイプラインステップ)
331
+ # =============================================================================
332
+
333
+ # ユーザー検証
334
+ class ValidateUserUseCase < SenroUsecaser::Base
335
+ class Input
336
+ #: (user_id: Integer, product_ids: Array[Integer], **untyped) -> void
337
+ def initialize(user_id:, product_ids:, **_rest)
338
+ @user_id = user_id #: Integer
339
+ @product_ids = product_ids #: Array[Integer]
340
+ end
341
+
342
+ #: () -> Integer
343
+ attr_reader :user_id
344
+
345
+ #: () -> Array[Integer]
346
+ attr_reader :product_ids
347
+ end
348
+
349
+ class Output
350
+ #: (user: User, product_ids: Array[Integer], **untyped) -> void
351
+ def initialize(user:, product_ids:, **_rest)
352
+ @user = user #: User
353
+ @product_ids = product_ids #: Array[Integer]
354
+ end
355
+
356
+ #: () -> User
357
+ attr_reader :user
358
+
359
+ #: () -> Array[Integer]
360
+ attr_reader :product_ids
361
+ end
362
+
363
+ depends_on :user_repository, UserRepository
364
+
365
+ # @rbs!
366
+ # def user_repository: () -> UserRepository
367
+
368
+ input Input
369
+ output Output
370
+
371
+ #: (Input) -> SenroUsecaser::Result[Output]
372
+ def call(input)
373
+ user = user_repository.find(input.user_id)
374
+ return failure(SenroUsecaser::Error.new(code: :user_not_found, message: "ユーザーが見つかりません")) unless user
375
+
376
+ success(Output.new(user: user, product_ids: input.product_ids))
377
+ end
378
+ end
379
+
380
+ # 商品検証と在庫チェック
381
+ class ValidateProductsUseCase < SenroUsecaser::Base
382
+ class Input
383
+ #: (user: User, product_ids: Array[Integer], **untyped) -> void
384
+ def initialize(user:, product_ids:, **_rest)
385
+ @user = user #: User
386
+ @product_ids = product_ids #: Array[Integer]
387
+ end
388
+
389
+ #: () -> User
390
+ attr_reader :user
391
+
392
+ #: () -> Array[Integer]
393
+ attr_reader :product_ids
394
+ end
395
+
396
+ class Output
397
+ #: (user: User, items: Array[Product], subtotal: Integer, **untyped) -> void
398
+ def initialize(user:, items:, subtotal:, **_rest)
399
+ @user = user #: User
400
+ @items = items #: Array[Product]
401
+ @subtotal = subtotal #: Integer
402
+ end
403
+
404
+ #: () -> User
405
+ attr_reader :user
406
+
407
+ #: () -> Array[Product]
408
+ attr_reader :items
409
+
410
+ #: () -> Integer
411
+ attr_reader :subtotal
412
+ end
413
+
414
+ depends_on :product_repository, ProductRepository
415
+
416
+ # @rbs!
417
+ # def product_repository: () -> ProductRepository
418
+
419
+ input Input
420
+ output Output
421
+
422
+ #: (Input) -> SenroUsecaser::Result[Output]
423
+ def call(input)
424
+ products = product_repository.find_all(input.product_ids)
425
+
426
+ if products.length != input.product_ids.length
427
+ return failure(SenroUsecaser::Error.new(code: :product_not_found, message: "商品が見つかりません"))
428
+ end
429
+
430
+ out_of_stock = products.select { |p| p.stock <= 0 }
431
+ if out_of_stock.any?
432
+ return failure(SenroUsecaser::Error.new(
433
+ code: :out_of_stock,
434
+ message: "在庫切れ: #{out_of_stock.map(&:name).join(", ")}"
435
+ ))
436
+ end
437
+
438
+ subtotal = products.sum(&:price)
439
+ success(Output.new(user: input.user, items: products, subtotal: subtotal))
440
+ end
441
+ end
442
+
443
+ # 税金計算
444
+ class CalculateTaxUseCase < SenroUsecaser::Base
445
+ class Input
446
+ #: (user: User, items: Array[Product], subtotal: Integer, **untyped) -> void
447
+ def initialize(user:, items:, subtotal:, **_rest)
448
+ @user = user #: User
449
+ @items = items #: Array[Product]
450
+ @subtotal = subtotal #: Integer
451
+ end
452
+
453
+ #: () -> User
454
+ attr_reader :user
455
+
456
+ #: () -> Array[Product]
457
+ attr_reader :items
458
+
459
+ #: () -> Integer
460
+ attr_reader :subtotal
461
+ end
462
+
463
+ class Output
464
+ #: (user: User, items: Array[Product], subtotal: Integer, tax: Integer, **untyped) -> void
465
+ def initialize(user:, items:, subtotal:, tax:, **_rest)
466
+ @user = user #: User
467
+ @items = items #: Array[Product]
468
+ @subtotal = subtotal #: Integer
469
+ @tax = tax #: Integer
470
+ end
471
+
472
+ #: () -> User
473
+ attr_reader :user
474
+
475
+ #: () -> Array[Product]
476
+ attr_reader :items
477
+
478
+ #: () -> Integer
479
+ attr_reader :subtotal
480
+
481
+ #: () -> Integer
482
+ attr_reader :tax
483
+ end
484
+
485
+ input Input
486
+ output Output
487
+
488
+ #: (Input) -> SenroUsecaser::Result[Output]
489
+ def call(input)
490
+ tax = (input.subtotal * 0.1).round
491
+ success(Output.new(user: input.user, items: input.items, subtotal: input.subtotal, tax: tax))
492
+ end
493
+ end
494
+
495
+ # プレミアム会員割引
496
+ class ApplyPremiumDiscountUseCase < SenroUsecaser::Base
497
+ class Input
498
+ #: (user: User, items: Array[Product], subtotal: Integer, tax: Integer, **untyped) -> void
499
+ def initialize(user:, items:, subtotal:, tax:, **_rest)
500
+ @user = user #: User
501
+ @items = items #: Array[Product]
502
+ @subtotal = subtotal #: Integer
503
+ @tax = tax #: Integer
504
+ end
505
+
506
+ #: () -> User
507
+ attr_reader :user
508
+
509
+ #: () -> Array[Product]
510
+ attr_reader :items
511
+
512
+ #: () -> Integer
513
+ attr_reader :subtotal
514
+
515
+ #: () -> Integer
516
+ attr_reader :tax
517
+ end
518
+
519
+ class Output
520
+ #: (user: User, items: Array[Product], subtotal: Integer, tax: Integer, discount: Integer, **untyped) -> void
521
+ def initialize(user:, items:, subtotal:, tax:, discount:, **_rest)
522
+ @user = user #: User
523
+ @items = items #: Array[Product]
524
+ @subtotal = subtotal #: Integer
525
+ @tax = tax #: Integer
526
+ @discount = discount #: Integer
527
+ end
528
+
529
+ #: () -> User
530
+ attr_reader :user
531
+
532
+ #: () -> Array[Product]
533
+ attr_reader :items
534
+
535
+ #: () -> Integer
536
+ attr_reader :subtotal
537
+
538
+ #: () -> Integer
539
+ attr_reader :tax
540
+
541
+ #: () -> Integer
542
+ attr_reader :discount
543
+ end
544
+
545
+ depends_on :discount_service, DiscountService
546
+
547
+ # @rbs!
548
+ # def discount_service: () -> DiscountService
549
+
550
+ input Input
551
+ output Output
552
+
553
+ #: (Input) -> SenroUsecaser::Result[Output]
554
+ def call(input)
555
+ discount = discount_service.calculate_premium_discount(input.subtotal)
556
+ success(Output.new(
557
+ user: input.user, items: input.items, subtotal: input.subtotal, tax: input.tax, discount: discount
558
+ ))
559
+ end
560
+ end
561
+
562
+ # 合計計算
563
+ class CalculateTotalUseCase < SenroUsecaser::Base
564
+ class Input
565
+ #: (user: User, items: Array[Product], subtotal: Integer, tax: Integer, ?discount: Integer, **untyped) -> void
566
+ def initialize(user:, items:, subtotal:, tax:, discount: 0, **_rest)
567
+ @user = user #: User
568
+ @items = items #: Array[Product]
569
+ @subtotal = subtotal #: Integer
570
+ @tax = tax #: Integer
571
+ @discount = discount #: Integer
572
+ end
573
+
574
+ #: () -> User
575
+ attr_reader :user
576
+
577
+ #: () -> Array[Product]
578
+ attr_reader :items
579
+
580
+ #: () -> Integer
581
+ attr_reader :subtotal
582
+
583
+ #: () -> Integer
584
+ attr_reader :tax
585
+
586
+ #: () -> Integer
587
+ attr_reader :discount
588
+ end
589
+
590
+ class Output
591
+ #: (user: User, items: Array[Product], subtotal: Integer, tax: Integer, discount: Integer, total: Integer, **untyped) -> void
592
+ def initialize(user:, items:, subtotal:, tax:, discount:, total:, **_rest)
593
+ @user = user #: User
594
+ @items = items #: Array[Product]
595
+ @subtotal = subtotal #: Integer
596
+ @tax = tax #: Integer
597
+ @discount = discount #: Integer
598
+ @total = total #: Integer
599
+ end
600
+
601
+ #: () -> User
602
+ attr_reader :user
603
+
604
+ #: () -> Array[Product]
605
+ attr_reader :items
606
+
607
+ #: () -> Integer
608
+ attr_reader :subtotal
609
+
610
+ #: () -> Integer
611
+ attr_reader :tax
612
+
613
+ #: () -> Integer
614
+ attr_reader :discount
615
+
616
+ #: () -> Integer
617
+ attr_reader :total
618
+ end
619
+
620
+ input Input
621
+ output Output
622
+
623
+ #: (Input) -> SenroUsecaser::Result[Output]
624
+ def call(input)
625
+ total = input.subtotal + input.tax - input.discount
626
+ success(Output.new(
627
+ user: input.user, items: input.items, subtotal: input.subtotal,
628
+ tax: input.tax, discount: input.discount, total: total
629
+ ))
630
+ end
631
+ end
632
+
633
+ # 決済処理
634
+ class ProcessPaymentUseCase < SenroUsecaser::Base
635
+ class Input
636
+ #: (user: User, items: Array[Product], subtotal: Integer, tax: Integer, discount: Integer, total: Integer, **untyped) -> void
637
+ def initialize(user:, items:, subtotal:, tax:, discount:, total:, **_rest)
638
+ @user = user #: User
639
+ @items = items #: Array[Product]
640
+ @subtotal = subtotal #: Integer
641
+ @tax = tax #: Integer
642
+ @discount = discount #: Integer
643
+ @total = total #: Integer
644
+ end
645
+
646
+ #: () -> User
647
+ attr_reader :user
648
+
649
+ #: () -> Array[Product]
650
+ attr_reader :items
651
+
652
+ #: () -> Integer
653
+ attr_reader :subtotal
654
+
655
+ #: () -> Integer
656
+ attr_reader :tax
657
+
658
+ #: () -> Integer
659
+ attr_reader :discount
660
+
661
+ #: () -> Integer
662
+ attr_reader :total
663
+ end
664
+
665
+ class Output
666
+ #: (user: User, items: Array[Product], subtotal: Integer, tax: Integer, discount: Integer, total: Integer, payment: PaymentResult, **untyped) -> void
667
+ def initialize(user:, items:, subtotal:, tax:, discount:, total:, payment:, **_rest)
668
+ @user = user #: User
669
+ @items = items #: Array[Product]
670
+ @subtotal = subtotal #: Integer
671
+ @tax = tax #: Integer
672
+ @discount = discount #: Integer
673
+ @total = total #: Integer
674
+ @payment = payment #: PaymentResult
675
+ end
676
+
677
+ #: () -> User
678
+ attr_reader :user
679
+
680
+ #: () -> Array[Product]
681
+ attr_reader :items
682
+
683
+ #: () -> Integer
684
+ attr_reader :subtotal
685
+
686
+ #: () -> Integer
687
+ attr_reader :tax
688
+
689
+ #: () -> Integer
690
+ attr_reader :discount
691
+
692
+ #: () -> Integer
693
+ attr_reader :total
694
+
695
+ #: () -> PaymentResult
696
+ attr_reader :payment
697
+ end
698
+
699
+ depends_on :payment_service, PaymentService
700
+
701
+ # @rbs!
702
+ # def payment_service: () -> PaymentService
703
+
704
+ input Input
705
+ output Output
706
+
707
+ #: (Input) -> SenroUsecaser::Result[Output]
708
+ def call(input)
709
+ payment_result = payment_service.charge(user: input.user, amount: input.total)
710
+ success(Output.new(
711
+ user: input.user, items: input.items, subtotal: input.subtotal,
712
+ tax: input.tax, discount: input.discount, total: input.total, payment: payment_result
713
+ ))
714
+ end
715
+ end
716
+
717
+ # 注文作成
718
+ class CreateOrderRecordUseCase < SenroUsecaser::Base
719
+ class Input
720
+ #: (user: User, items: Array[Product], subtotal: Integer, tax: Integer, discount: Integer, total: Integer, **untyped) -> void
721
+ def initialize(user:, items:, subtotal:, tax:, discount:, total:, **_rest)
722
+ @user = user #: User
723
+ @items = items #: Array[Product]
724
+ @subtotal = subtotal #: Integer
725
+ @tax = tax #: Integer
726
+ @discount = discount #: Integer
727
+ @total = total #: Integer
728
+ end
729
+
730
+ #: () -> User
731
+ attr_reader :user
732
+
733
+ #: () -> Array[Product]
734
+ attr_reader :items
735
+
736
+ #: () -> Integer
737
+ attr_reader :subtotal
738
+
739
+ #: () -> Integer
740
+ attr_reader :tax
741
+
742
+ #: () -> Integer
743
+ attr_reader :discount
744
+
745
+ #: () -> Integer
746
+ attr_reader :total
747
+ end
748
+
749
+ class Output
750
+ #: (user: User, order: Order, **untyped) -> void
751
+ def initialize(user:, order:, **_rest)
752
+ @user = user #: User
753
+ @order = order #: Order
754
+ end
755
+
756
+ #: () -> User
757
+ attr_reader :user
758
+
759
+ #: () -> Order
760
+ attr_reader :order
761
+ end
762
+
763
+ depends_on :order_repository, OrderRepository
764
+
765
+ # @rbs!
766
+ # def order_repository: () -> OrderRepository
767
+
768
+ input Input
769
+ output Output
770
+
771
+ #: (Input) -> SenroUsecaser::Result[Output]
772
+ def call(input)
773
+ order = order_repository.create(
774
+ user_id: input.user.id,
775
+ items: input.items.map(&:id),
776
+ subtotal: input.subtotal,
777
+ tax: input.tax,
778
+ discount: input.discount,
779
+ total: input.total
780
+ )
781
+ success(Output.new(user: input.user, order: order))
782
+ end
783
+ end
784
+
785
+ # 通知送信
786
+ class SendOrderNotificationUseCase < SenroUsecaser::Base
787
+ class Input
788
+ #: (user: User, order: Order, **untyped) -> void
789
+ def initialize(user:, order:, **_rest)
790
+ @user = user #: User
791
+ @order = order #: Order
792
+ end
793
+
794
+ #: () -> User
795
+ attr_reader :user
796
+
797
+ #: () -> Order
798
+ attr_reader :order
799
+ end
800
+
801
+ class Output
802
+ #: (user: User, order: Order, notified: bool, **untyped) -> void
803
+ def initialize(user:, order:, notified:, **_rest)
804
+ @user = user #: User
805
+ @order = order #: Order
806
+ @notified = notified #: bool
807
+ end
808
+
809
+ #: () -> User
810
+ attr_reader :user
811
+
812
+ #: () -> Order
813
+ attr_reader :order
814
+
815
+ #: () -> bool
816
+ attr_reader :notified
817
+ end
818
+
819
+ depends_on :notification_service, NotificationService
820
+
821
+ # @rbs!
822
+ # def notification_service: () -> NotificationService
823
+
824
+ input Input
825
+ output Output
826
+
827
+ #: (Input) -> SenroUsecaser::Result[Output]
828
+ def call(input)
829
+ notification_service.send_email(
830
+ to: input.user.email,
831
+ subject: "ご注文ありがとうございます(注文番号: #{input.order.id})",
832
+ body: "ご注文を承りました。"
833
+ )
834
+ success(Output.new(user: input.user, order: input.order, notified: true))
835
+ end
836
+ end
837
+
838
+ # パイプライン最終出力をラップ
839
+ class WrapOrderOutputUseCase < SenroUsecaser::Base
840
+ class Input
841
+ #: (order: Order, **untyped) -> void
842
+ def initialize(order:, **_rest)
843
+ @order = order #: Order
844
+ end
845
+
846
+ #: () -> Order
847
+ attr_reader :order
848
+ end
849
+
850
+ input Input
851
+ output CreateOrderOutput
852
+
853
+ #: (Input) -> SenroUsecaser::Result[CreateOrderOutput]
854
+ def call(input)
855
+ success(CreateOrderOutput.new(order: input.order))
856
+ end
857
+ end
858
+
859
+ # =============================================================================
860
+ # 複合 UseCase(Pipeline)
861
+ # =============================================================================
862
+
863
+ # ログ記録用 Extension
864
+ module LoggingExtension
865
+ #: (Hash[Symbol, untyped]) -> void
866
+ def self.before(context)
867
+ puts " [Logging] UseCase 開始: #{context.keys.join(", ")}"
868
+ end
869
+
870
+ #: (Hash[Symbol, untyped], SenroUsecaser::Result[untyped]) -> void
871
+ def self.after(_context, result)
872
+ status = result.success? ? "成功" : "失敗"
873
+ puts " [Logging] UseCase 終了: #{status}"
874
+ end
875
+ end
876
+
877
+ # 注文作成パイプライン
878
+ class CreateOrderUseCase < SenroUsecaser::Base
879
+ class Input
880
+ #: (user_id: Integer, product_ids: Array[Integer], **untyped) -> void
881
+ def initialize(user_id:, product_ids:, **_rest)
882
+ @user_id = user_id #: Integer
883
+ @product_ids = product_ids #: Array[Integer]
884
+ end
885
+
886
+ #: () -> Integer
887
+ attr_reader :user_id
888
+
889
+ #: () -> Array[Integer]
890
+ attr_reader :product_ids
891
+ end
892
+
893
+ extend_with LoggingExtension
894
+
895
+ input Input
896
+ output CreateOrderOutput
897
+
898
+ # @rbs!
899
+ # def self.call: (Input, ?container: SenroUsecaser::Container) -> SenroUsecaser::Result[CreateOrderOutput]
900
+
901
+ organize do
902
+ step ValidateUserUseCase
903
+ step ValidateProductsUseCase
904
+ step CalculateTaxUseCase
905
+ step ApplyPremiumDiscountUseCase, if: :premium_user?
906
+ step CalculateTotalUseCase
907
+ step ProcessPaymentUseCase
908
+ step CreateOrderRecordUseCase
909
+ step SendOrderNotificationUseCase, on_failure: :continue
910
+ step WrapOrderOutputUseCase
911
+ end
912
+
913
+ #: (Hash[Symbol, untyped]) -> bool
914
+ def premium_user?(context)
915
+ user = context[:user] #: User?
916
+ user&.premium? || false
917
+ end
918
+ end
919
+
920
+ # =============================================================================
921
+ # サンプル実行
922
+ # =============================================================================
923
+
924
+ # 設定と起動
925
+ SenroUsecaser.configure do |config|
926
+ config.providers = [
927
+ CoreProvider,
928
+ InfrastructureProvider,
929
+ ServiceProvider,
930
+ DevelopmentProvider
931
+ ]
932
+ end
933
+
934
+ puts "1. Provider の起動"
935
+ puts "-" * 70
936
+ SenroUsecaser.boot!
937
+ puts
938
+
939
+ # -----------------------------------------------------------------------------
940
+ puts "2. Input Class の確認"
941
+ puts "-" * 70
942
+
943
+ input_class = CreateOrderUseCase.input_class
944
+ puts " 入力クラス: #{input_class}"
945
+ puts
946
+
947
+ # -----------------------------------------------------------------------------
948
+ puts "3. 正常ケース: プレミアム会員の注文"
949
+ puts "-" * 70
950
+
951
+ input = CreateOrderUseCase::Input.new(user_id: 1, product_ids: [101, 102])
952
+ result = CreateOrderUseCase.call(input)
953
+
954
+ if result.success?
955
+ output = result.value!
956
+ puts " 注文成功!"
957
+ puts " 注文ID: #{output.order.id}"
958
+ puts " 小計: ¥#{output.order.subtotal}"
959
+ puts " 税金: ¥#{output.order.tax}"
960
+ puts " 割引: ¥#{output.order.discount} (プレミアム会員)"
961
+ puts " 合計: ¥#{output.order.total}"
962
+ else
963
+ puts " 注文失敗: #{result.errors.map(&:message).join(", ")}"
964
+ end
965
+ puts
966
+
967
+ # -----------------------------------------------------------------------------
968
+ puts "4. 正常ケース: 一般会員の注文(割引なし)"
969
+ puts "-" * 70
970
+
971
+ input = CreateOrderUseCase::Input.new(user_id: 2, product_ids: [102])
972
+ result = CreateOrderUseCase.call(input)
973
+
974
+ if result.success?
975
+ output = result.value!
976
+ puts " 注文成功!"
977
+ puts " 注文ID: #{output.order.id}"
978
+ puts " 小計: ¥#{output.order.subtotal}"
979
+ puts " 税金: ¥#{output.order.tax}"
980
+ puts " 割引: ¥#{output.order.discount} (一般会員)"
981
+ puts " 合計: ¥#{output.order.total}"
982
+ else
983
+ puts " 注文失敗: #{result.errors.map(&:message).join(", ")}"
984
+ end
985
+ puts
986
+
987
+ # -----------------------------------------------------------------------------
988
+ puts "5. 失敗ケース: 存在しないユーザー"
989
+ puts "-" * 70
990
+
991
+ input = CreateOrderUseCase::Input.new(user_id: 999, product_ids: [101])
992
+ result = CreateOrderUseCase.call(input)
993
+
994
+ if result.failure?
995
+ error = result.errors.first #: SenroUsecaser::Error?
996
+ puts " 期待通り失敗: #{error&.code} - #{error&.message}" if error
997
+ end
998
+ puts
999
+
1000
+ # -----------------------------------------------------------------------------
1001
+ puts "6. 失敗ケース: 在庫切れ商品"
1002
+ puts "-" * 70
1003
+
1004
+ input = CreateOrderUseCase::Input.new(user_id: 1, product_ids: [103])
1005
+ result = CreateOrderUseCase.call(input)
1006
+
1007
+ if result.failure?
1008
+ error = result.errors.first #: SenroUsecaser::Error?
1009
+ puts " 期待通り失敗: #{error&.code} - #{error&.message}" if error
1010
+ end
1011
+ puts
1012
+
1013
+ # -----------------------------------------------------------------------------
1014
+ puts "7. Scoped Container: リクエストスコープ"
1015
+ puts "-" * 70
1016
+
1017
+ # リクエストごとに current_user を注入するパターン
1018
+ class CurrentUserAwareUseCase < SenroUsecaser::Base
1019
+ depends_on :current_user, User
1020
+ depends_on :logger, Logger
1021
+
1022
+ # @rbs!
1023
+ # def current_user: () -> User
1024
+ # def logger: () -> Logger
1025
+
1026
+ output GreetingOutput
1027
+
1028
+ #: (?untyped, **untyped) -> SenroUsecaser::Result[GreetingOutput]
1029
+ def call(_input = nil, **_args)
1030
+ logger.info("現在のユーザー: #{current_user.name}")
1031
+ success(GreetingOutput.new(greeted: "こんにちは、#{current_user.name}さん!"))
1032
+ end
1033
+ end
1034
+
1035
+ # リクエストスコープのコンテナを作成
1036
+ current_user = User.new(id: 1, name: "リクエストユーザー", email: "request@example.com", premium: false)
1037
+ scoped_container = SenroUsecaser.container.scope do
1038
+ # @type self: SenroUsecaser::Container
1039
+ register(:current_user, current_user)
1040
+ end
1041
+
1042
+ result = CurrentUserAwareUseCase.call(container: scoped_container)
1043
+ if result.success?
1044
+ value = result.value!
1045
+ puts " #{value.greeted}"
1046
+ end
1047
+ puts
1048
+
1049
+ # -----------------------------------------------------------------------------
1050
+ puts "8. Result の操作"
1051
+ puts "-" * 70
1052
+
1053
+ success_result = SenroUsecaser::Result.success({ value: 42 })
1054
+ failure_result = SenroUsecaser::Result.failure(
1055
+ SenroUsecaser::Error.new(code: :error1, message: "エラー1"),
1056
+ SenroUsecaser::Error.new(code: :error2, message: "エラー2")
1057
+ )
1058
+
1059
+ puts " success?: #{success_result.success?}, failure?: #{failure_result.failure?}"
1060
+ puts " value_or: #{failure_result.value_or("デフォルト値")}"
1061
+
1062
+ mapped_result = success_result.map do |v|
1063
+ hash = v #: Hash[Symbol, Integer]
1064
+ hash[:value] * 2
1065
+ end
1066
+ puts " map: #{mapped_result.value}"
1067
+ puts " エラー数: #{failure_result.errors.length}"
1068
+ puts
1069
+
1070
+ # -----------------------------------------------------------------------------
1071
+ puts "9. Accumulated Context の確認"
1072
+ puts "-" * 70
1073
+
1074
+ class Step1 < SenroUsecaser::Base
1075
+ class Input
1076
+ #: (initial: String, **untyped) -> void
1077
+ def initialize(initial:, **_rest)
1078
+ @initial = initial #: String
1079
+ end
1080
+
1081
+ #: () -> String
1082
+ attr_reader :initial
1083
+ end
1084
+
1085
+ class Output
1086
+ #: (step1_data: String, counter: Integer, **untyped) -> void
1087
+ def initialize(step1_data:, counter:, **_rest)
1088
+ @step1_data = step1_data #: String
1089
+ @counter = counter #: Integer
1090
+ end
1091
+
1092
+ #: () -> String
1093
+ attr_reader :step1_data
1094
+
1095
+ #: () -> Integer
1096
+ attr_reader :counter
1097
+ end
1098
+
1099
+ input Input
1100
+ output Output
1101
+
1102
+ #: (Input) -> SenroUsecaser::Result[Output]
1103
+ def call(_input)
1104
+ success(Output.new(step1_data: "from step1", counter: 1))
1105
+ end
1106
+ end
1107
+
1108
+ class Step2 < SenroUsecaser::Base
1109
+ class Input
1110
+ #: (step1_data: String, counter: Integer, **untyped) -> void
1111
+ def initialize(step1_data:, counter:, **_rest)
1112
+ @step1_data = step1_data #: String
1113
+ @counter = counter #: Integer
1114
+ end
1115
+
1116
+ #: () -> String
1117
+ attr_reader :step1_data
1118
+
1119
+ #: () -> Integer
1120
+ attr_reader :counter
1121
+ end
1122
+
1123
+ class Output
1124
+ #: (step1_data: String, step2_data: String, counter: Integer, **untyped) -> void
1125
+ def initialize(step1_data:, step2_data:, counter:, **_rest)
1126
+ @step1_data = step1_data #: String
1127
+ @step2_data = step2_data #: String
1128
+ @counter = counter #: Integer
1129
+ end
1130
+
1131
+ #: () -> String
1132
+ attr_reader :step1_data
1133
+
1134
+ #: () -> String
1135
+ attr_reader :step2_data
1136
+
1137
+ #: () -> Integer
1138
+ attr_reader :counter
1139
+ end
1140
+
1141
+ input Input
1142
+ output Output
1143
+
1144
+ #: (Input) -> SenroUsecaser::Result[Output]
1145
+ def call(input)
1146
+ success(Output.new(step1_data: input.step1_data, step2_data: "from step2", counter: input.counter + 1))
1147
+ end
1148
+ end
1149
+
1150
+ class Step3 < SenroUsecaser::Base
1151
+ class Input
1152
+ #: (step1_data: String, step2_data: String, counter: Integer, **untyped) -> void
1153
+ def initialize(step1_data:, step2_data:, counter:, **_rest)
1154
+ @step1_data = step1_data #: String
1155
+ @step2_data = step2_data #: String
1156
+ @counter = counter #: Integer
1157
+ end
1158
+
1159
+ #: () -> String
1160
+ attr_reader :step1_data
1161
+
1162
+ #: () -> String
1163
+ attr_reader :step2_data
1164
+
1165
+ #: () -> Integer
1166
+ attr_reader :counter
1167
+ end
1168
+
1169
+ class Output
1170
+ #: (step1_data: String, step2_data: String, step3_data: String, counter: Integer, final: bool, **untyped) -> void
1171
+ def initialize(step1_data:, step2_data:, step3_data:, counter:, final:, **_rest)
1172
+ @step1_data = step1_data #: String
1173
+ @step2_data = step2_data #: String
1174
+ @step3_data = step3_data #: String
1175
+ @counter = counter #: Integer
1176
+ @final = final #: bool
1177
+ end
1178
+
1179
+ #: () -> String
1180
+ attr_reader :step1_data
1181
+
1182
+ #: () -> String
1183
+ attr_reader :step2_data
1184
+
1185
+ #: () -> String
1186
+ attr_reader :step3_data
1187
+
1188
+ #: () -> Integer
1189
+ attr_reader :counter
1190
+
1191
+ #: () -> bool
1192
+ attr_reader :final
1193
+ end
1194
+
1195
+ input Input
1196
+ output Output
1197
+
1198
+ #: (Input) -> SenroUsecaser::Result[Output]
1199
+ def call(input)
1200
+ success(Output.new(
1201
+ step1_data: input.step1_data, step2_data: input.step2_data,
1202
+ step3_data: "from step3", counter: input.counter + 1, final: true
1203
+ ))
1204
+ end
1205
+ end
1206
+
1207
+ class WrapAccumulatedOutputUseCase < SenroUsecaser::Base
1208
+ class Input
1209
+ #: (counter: Integer, final: bool, **untyped) -> void
1210
+ def initialize(counter:, final:, **_rest)
1211
+ @counter = counter #: Integer
1212
+ @final = final #: bool
1213
+ end
1214
+
1215
+ #: () -> Integer
1216
+ attr_reader :counter
1217
+
1218
+ #: () -> bool
1219
+ attr_reader :final
1220
+ end
1221
+
1222
+ input Input
1223
+ output AccumulatedOutput
1224
+
1225
+ #: (Input) -> SenroUsecaser::Result[AccumulatedOutput]
1226
+ def call(input)
1227
+ success(AccumulatedOutput.new(counter: input.counter, final: input.final))
1228
+ end
1229
+ end
1230
+
1231
+ class AccumulatedContextDemo < SenroUsecaser::Base
1232
+ class Input
1233
+ #: (initial: String, **untyped) -> void
1234
+ def initialize(initial:, **_rest)
1235
+ @initial = initial #: String
1236
+ end
1237
+
1238
+ #: () -> String
1239
+ attr_reader :initial
1240
+ end
1241
+
1242
+ input Input
1243
+ output AccumulatedOutput
1244
+
1245
+ # @rbs!
1246
+ # def self.call: (Input, ?container: SenroUsecaser::Container) -> SenroUsecaser::Result[AccumulatedOutput]
1247
+
1248
+ organize do
1249
+ step Step1
1250
+ step Step2, if: :check_accumulated
1251
+ step Step3
1252
+ step WrapAccumulatedOutputUseCase
1253
+ end
1254
+
1255
+ #: (untyped) -> bool
1256
+ def check_accumulated(ctx)
1257
+ puts " [InputChaining] step1_data: #{ctx.step1_data}"
1258
+ true
1259
+ end
1260
+ end
1261
+
1262
+ input = AccumulatedContextDemo::Input.new(initial: "value")
1263
+ result = AccumulatedContextDemo.call(input) #: SenroUsecaser::Result[AccumulatedOutput]
1264
+ if result.success?
1265
+ output = result.value!
1266
+ puts " 最終結果: counter=#{output.counter}, final=#{output.final}"
1267
+ end
1268
+ puts
1269
+
1270
+ # -----------------------------------------------------------------------------
1271
+ puts "10. シャットダウン"
1272
+ puts "-" * 70
1273
+ SenroUsecaser.shutdown!
1274
+ puts " 完了"
1275
+ puts
1276
+
1277
+ puts "=" * 70
1278
+ puts "すべてのサンプルが完了しました"
1279
+ puts "=" * 70