senro_usecaser 0.1.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +52 -0
- data/README.md +282 -6
- data/lib/senro_usecaser/base.rb +136 -20
- data/lib/senro_usecaser/hook.rb +180 -0
- data/lib/senro_usecaser/provider.rb +1 -1
- data/lib/senro_usecaser/version.rb +1 -1
- data/lib/senro_usecaser.rb +1 -0
- data/sig/generated/senro_usecaser/base.rbs +60 -10
- data/sig/generated/senro_usecaser/hook.rbs +112 -0
- data/sig/generated/senro_usecaser/provider.rbs +1 -1
- data/sig/overrides.rbs +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cbfc07ede41cc295ceb0a019774a480744b226e88edc46f72a6447d59375f4e4
|
|
4
|
+
data.tar.gz: b86aaade8ccc9f2479f18d0f3bd5204203718ce26740eb3a51b5cccf1ed06798
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2ce95671ffefc951140c3625db940f1b077fe8bf40138d790b25067ab010a087d9f96b6c2e31bab2819ea89f0b2e6e905c70288f9409e24782cd73a0bca832cd
|
|
7
|
+
data.tar.gz: aa3b27b6eff694929f8cfc33ca47e5d7fb450eeb4ac5da9fecba2023cc70393a2edfa1b033e89d0fe0f0e1abe5cda3817841e8b92e04294062c0cd4a02ef8879
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.3.0] - 2026-01-31
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Runtime type validation for input**
|
|
13
|
+
- `input` now accepts Module(s) for interface-based validation
|
|
14
|
+
- Single module: `input HasUserId` - validates that input's class includes the module
|
|
15
|
+
- Multiple modules: `input HasUserId, HasEmail` - validates that input's class includes all modules
|
|
16
|
+
- Class validation remains supported for backwards compatibility
|
|
17
|
+
- **Runtime type validation for output**
|
|
18
|
+
- When `output` is declared with a Class, the success result's value is validated
|
|
19
|
+
- Raises `TypeError` if the output value is not an instance of the declared class
|
|
20
|
+
- Hash schema (`output({ key: Type })`) skips validation for backwards compatibility
|
|
21
|
+
- **New class methods**
|
|
22
|
+
- `input_types` - Returns an array of declared input types (Module/Class)
|
|
23
|
+
- `input_class` - Backwards compatible method, returns Class if specified or first type
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Type validation errors raise exceptions with `.call` and return `Result.failure` with `.call!`
|
|
28
|
+
- Input validation: `ArgumentError`
|
|
29
|
+
- Output validation: `TypeError`
|
|
30
|
+
|
|
31
|
+
## [0.2.0] - 2026-01-31
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- **Hook class with dependency injection** - New `SenroUsecaser::Hook` base class for creating hooks with `depends_on` support
|
|
36
|
+
- Supports `namespace` declaration for scoped dependency resolution
|
|
37
|
+
- Supports `infer_namespace_from_module` configuration
|
|
38
|
+
- Can inherit namespace from the UseCase when not explicitly declared
|
|
39
|
+
- **Block hooks can access dependencies**
|
|
40
|
+
- `before` and `after` blocks now run in instance context via `instance_exec`
|
|
41
|
+
- Dependencies declared with `depends_on` are directly accessible in block hooks
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- **Around block signature** - Changed from `|input, &block|` to `|input, use_case, &block|`
|
|
46
|
+
- The `use_case` argument provides access to dependencies
|
|
47
|
+
- **Input class is now mandatory** - All UseCases must define an `input` class
|
|
48
|
+
- **Renamed `context` to `input`** - Hook parameters renamed for clarity
|
|
49
|
+
|
|
50
|
+
## [0.1.0] - 2026-01-30
|
|
51
|
+
|
|
52
|
+
- Initial release
|
data/README.md
CHANGED
|
@@ -442,6 +442,179 @@ end
|
|
|
442
442
|
|
|
443
443
|
The `**_rest` parameter in Input's initialize allows extra fields to be passed through pipeline steps without errors.
|
|
444
444
|
|
|
445
|
+
### Runtime Type Validation
|
|
446
|
+
|
|
447
|
+
In addition to static type checking with RBS, SenroUsecaser provides runtime type validation for Input and Output. This ensures that the actual values passed at runtime match the expected types.
|
|
448
|
+
|
|
449
|
+
#### Input Type Validation
|
|
450
|
+
|
|
451
|
+
The `input` declaration supports three patterns:
|
|
452
|
+
|
|
453
|
+
##### 1. Class Validation (Traditional)
|
|
454
|
+
|
|
455
|
+
When a Class is specified, input must be an instance of that class:
|
|
456
|
+
|
|
457
|
+
```ruby
|
|
458
|
+
class CreateUserUseCase < SenroUsecaser::Base
|
|
459
|
+
input CreateUserInput # Class
|
|
460
|
+
|
|
461
|
+
def call(input)
|
|
462
|
+
# input must be a CreateUserInput instance
|
|
463
|
+
success(input.name)
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# OK
|
|
468
|
+
CreateUserUseCase.call(CreateUserInput.new(name: "Taro"))
|
|
469
|
+
|
|
470
|
+
# ArgumentError: Input must be an instance of CreateUserInput, got String
|
|
471
|
+
CreateUserUseCase.call("invalid")
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
##### 2. Interface Validation (Single Module)
|
|
475
|
+
|
|
476
|
+
When a Module is specified, input's class must include that module. This enables duck-typing with explicit interface contracts:
|
|
477
|
+
|
|
478
|
+
```ruby
|
|
479
|
+
# Define interface
|
|
480
|
+
module HasUserId
|
|
481
|
+
def user_id
|
|
482
|
+
raise NotImplementedError
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# UseCase expects input that includes HasUserId
|
|
487
|
+
class FindUserUseCase < SenroUsecaser::Base
|
|
488
|
+
input HasUserId
|
|
489
|
+
|
|
490
|
+
#: (HasUserId) -> SenroUsecaser::Result[User]
|
|
491
|
+
def call(input)
|
|
492
|
+
user = User.find(input.user_id)
|
|
493
|
+
success(user)
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Input class that implements the interface
|
|
498
|
+
class UserQuery
|
|
499
|
+
include HasUserId
|
|
500
|
+
|
|
501
|
+
attr_reader :user_id
|
|
502
|
+
|
|
503
|
+
def initialize(user_id:)
|
|
504
|
+
@user_id = user_id
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# OK - UserQuery includes HasUserId
|
|
509
|
+
FindUserUseCase.call(UserQuery.new(user_id: 123))
|
|
510
|
+
|
|
511
|
+
# ArgumentError: Input UserQuery must include HasUserId
|
|
512
|
+
class InvalidInput
|
|
513
|
+
attr_reader :user_id
|
|
514
|
+
def initialize(user_id:) = @user_id = user_id
|
|
515
|
+
end
|
|
516
|
+
FindUserUseCase.call(InvalidInput.new(user_id: 123))
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
##### 3. Multiple Interfaces Validation
|
|
520
|
+
|
|
521
|
+
Multiple Modules can be specified. The input must include ALL of them:
|
|
522
|
+
|
|
523
|
+
```ruby
|
|
524
|
+
module HasUserId
|
|
525
|
+
def user_id = raise NotImplementedError
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
module HasEmail
|
|
529
|
+
def email = raise NotImplementedError
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# UseCase requires both interfaces
|
|
533
|
+
class NotifyUserUseCase < SenroUsecaser::Base
|
|
534
|
+
input HasUserId, HasEmail
|
|
535
|
+
|
|
536
|
+
#: ((HasUserId & HasEmail)) -> SenroUsecaser::Result[bool]
|
|
537
|
+
def call(input)
|
|
538
|
+
notify(input.user_id, input.email)
|
|
539
|
+
success(true)
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# Input class must include both modules
|
|
544
|
+
class NotificationRequest
|
|
545
|
+
include HasUserId
|
|
546
|
+
include HasEmail
|
|
547
|
+
|
|
548
|
+
attr_reader :user_id, :email
|
|
549
|
+
|
|
550
|
+
def initialize(user_id:, email:)
|
|
551
|
+
@user_id = user_id
|
|
552
|
+
@email = email
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# OK
|
|
557
|
+
NotifyUserUseCase.call(NotificationRequest.new(user_id: 123, email: "test@example.com"))
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
##### Interface Pattern in Pipelines
|
|
561
|
+
|
|
562
|
+
Interface validation is especially useful for sub-UseCases in pipelines. A parent UseCase's Input can include multiple interfaces, and each step only requires the interfaces it needs:
|
|
563
|
+
|
|
564
|
+
```ruby
|
|
565
|
+
# Parent UseCase - Input includes both interfaces
|
|
566
|
+
class ProcessOrderUseCase < SenroUsecaser::Base
|
|
567
|
+
class Input
|
|
568
|
+
include HasUserId
|
|
569
|
+
include HasEmail
|
|
570
|
+
|
|
571
|
+
attr_reader :user_id, :email, :order_items
|
|
572
|
+
|
|
573
|
+
def initialize(user_id:, email:, order_items:)
|
|
574
|
+
@user_id = user_id
|
|
575
|
+
@email = email
|
|
576
|
+
@order_items = order_items
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
input Input
|
|
581
|
+
|
|
582
|
+
organize do
|
|
583
|
+
step FindUserUseCase # Only needs HasUserId
|
|
584
|
+
step NotifyUserUseCase # Needs HasUserId and HasEmail
|
|
585
|
+
step CreateOrderUseCase
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
#### Output Type Validation
|
|
591
|
+
|
|
592
|
+
When `output` is declared with a Class, the success result's value is validated:
|
|
593
|
+
|
|
594
|
+
```ruby
|
|
595
|
+
class UserOutput
|
|
596
|
+
attr_reader :user
|
|
597
|
+
def initialize(user:) = @user = user
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
class FindUserUseCase < SenroUsecaser::Base
|
|
601
|
+
input FindUserInput
|
|
602
|
+
output UserOutput # Class declaration enables validation
|
|
603
|
+
|
|
604
|
+
def call(input)
|
|
605
|
+
user = User.find(input.user_id)
|
|
606
|
+
success(UserOutput.new(user: user)) # OK
|
|
607
|
+
|
|
608
|
+
# TypeError: Output must be an instance of UserOutput, got User
|
|
609
|
+
# success(user) # Wrong! Must wrap in UserOutput
|
|
610
|
+
end
|
|
611
|
+
end
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
**Note:** When `output` is a Hash schema (e.g., `output({ user: User })`), validation is skipped for backwards compatibility.
|
|
615
|
+
|
|
616
|
+
**Note:** Type validation errors raise exceptions (`ArgumentError` for input, `TypeError` for output). See [`.call` vs `.call!`](#call-vs-call-1) for how exceptions are handled.
|
|
617
|
+
|
|
445
618
|
### Simplicity
|
|
446
619
|
|
|
447
620
|
Define UseCases with minimal boilerplate. Avoids over-abstraction and provides an intuitive API.
|
|
@@ -659,20 +832,30 @@ end
|
|
|
659
832
|
|
|
660
833
|
##### Block Syntax
|
|
661
834
|
|
|
835
|
+
Block hooks are executed in the UseCase instance context, allowing access to `depends_on` dependencies.
|
|
836
|
+
|
|
662
837
|
```ruby
|
|
663
838
|
class CreateUserUseCase < SenroUsecaser::Base
|
|
839
|
+
depends_on :logger
|
|
840
|
+
depends_on :metrics
|
|
841
|
+
input Input
|
|
842
|
+
|
|
843
|
+
# before/after blocks can access dependencies directly
|
|
664
844
|
before do |input|
|
|
665
|
-
|
|
845
|
+
logger.info("Starting with #{input.class.name}")
|
|
666
846
|
end
|
|
667
847
|
|
|
668
848
|
after do |input, result|
|
|
669
|
-
#
|
|
849
|
+
logger.info("Finished: #{result.success? ? 'success' : 'failure'}")
|
|
850
|
+
metrics.increment(:use_case_completed)
|
|
670
851
|
end
|
|
671
852
|
|
|
672
|
-
around
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
853
|
+
# around block receives use_case as second argument for dependency access
|
|
854
|
+
around do |input, use_case, &block|
|
|
855
|
+
use_case.logger.info("Transaction start")
|
|
856
|
+
result = ActiveRecord::Base.transaction { block.call }
|
|
857
|
+
use_case.logger.info("Transaction end")
|
|
858
|
+
result
|
|
676
859
|
end
|
|
677
860
|
|
|
678
861
|
def call(input)
|
|
@@ -681,6 +864,77 @@ class CreateUserUseCase < SenroUsecaser::Base
|
|
|
681
864
|
end
|
|
682
865
|
```
|
|
683
866
|
|
|
867
|
+
##### Hook Classes
|
|
868
|
+
|
|
869
|
+
For more complex hooks with their own dependencies, use `SenroUsecaser::Hook` class:
|
|
870
|
+
|
|
871
|
+
```ruby
|
|
872
|
+
class LoggingHook < SenroUsecaser::Hook
|
|
873
|
+
depends_on :logger
|
|
874
|
+
depends_on :metrics
|
|
875
|
+
|
|
876
|
+
def before(input)
|
|
877
|
+
logger.info("Starting with #{input.class.name}")
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
def after(input, result)
|
|
881
|
+
logger.info("Finished: #{result.success? ? 'success' : 'failure'}")
|
|
882
|
+
metrics.increment(:use_case_completed)
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
def around(input)
|
|
886
|
+
logger.info("Around start")
|
|
887
|
+
result = yield
|
|
888
|
+
logger.info("Around end")
|
|
889
|
+
result
|
|
890
|
+
end
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
class CreateUserUseCase < SenroUsecaser::Base
|
|
894
|
+
extend_with LoggingHook
|
|
895
|
+
|
|
896
|
+
def call(input)
|
|
897
|
+
# main logic
|
|
898
|
+
end
|
|
899
|
+
end
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
Hook classes support:
|
|
903
|
+
- `depends_on` for dependency injection
|
|
904
|
+
- `namespace` for scoped dependency resolution
|
|
905
|
+
- Automatic namespace inference from module structure (when `infer_namespace_from_module` is enabled)
|
|
906
|
+
- Inheriting namespace from the UseCase if not explicitly declared
|
|
907
|
+
|
|
908
|
+
```ruby
|
|
909
|
+
# Hook with explicit namespace
|
|
910
|
+
class Admin::AuditHook < SenroUsecaser::Hook
|
|
911
|
+
namespace :admin
|
|
912
|
+
depends_on :audit_logger
|
|
913
|
+
|
|
914
|
+
def after(input, result)
|
|
915
|
+
audit_logger.log(action: "create", success: result.success?)
|
|
916
|
+
end
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
# Hook inheriting namespace from UseCase
|
|
920
|
+
class MetricsHook < SenroUsecaser::Hook
|
|
921
|
+
depends_on :metrics # resolved from UseCase's namespace
|
|
922
|
+
|
|
923
|
+
def after(input, result)
|
|
924
|
+
metrics.increment(:completed)
|
|
925
|
+
end
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
class Admin::CreateUserUseCase < SenroUsecaser::Base
|
|
929
|
+
namespace :admin
|
|
930
|
+
extend_with MetricsHook # metrics resolved from :admin namespace
|
|
931
|
+
|
|
932
|
+
def call(input)
|
|
933
|
+
# ...
|
|
934
|
+
end
|
|
935
|
+
end
|
|
936
|
+
```
|
|
937
|
+
|
|
684
938
|
##### Input/Output Validation
|
|
685
939
|
|
|
686
940
|
Use `extend_with` to integrate validation libraries like ActiveModel::Validations:
|
|
@@ -926,6 +1180,28 @@ end
|
|
|
926
1180
|
|
|
927
1181
|
Use `.call!` when you want to ensure all exceptions are captured as `Result.failure` without explicit rescue blocks in your UseCase.
|
|
928
1182
|
|
|
1183
|
+
**Type validation errors** (from `input` and `output` declarations) also follow this pattern:
|
|
1184
|
+
|
|
1185
|
+
```ruby
|
|
1186
|
+
# With .call - type validation errors raise exceptions
|
|
1187
|
+
begin
|
|
1188
|
+
UseCase.call(invalid_input)
|
|
1189
|
+
rescue ArgumentError => e
|
|
1190
|
+
puts e.message # "Input SomeClass must include HasUserId"
|
|
1191
|
+
end
|
|
1192
|
+
|
|
1193
|
+
# With .call! - type validation errors become Result.failure
|
|
1194
|
+
result = UseCase.call!(invalid_input)
|
|
1195
|
+
result.failure? # => true
|
|
1196
|
+
result.errors.first.code # => :exception
|
|
1197
|
+
result.errors.first.message # => "Input SomeClass must include HasUserId"
|
|
1198
|
+
```
|
|
1199
|
+
|
|
1200
|
+
| Validation | Exception type | With `.call` | With `.call!` |
|
|
1201
|
+
|------------|---------------|--------------|---------------|
|
|
1202
|
+
| Input type | `ArgumentError` | Raises | `Result.failure` |
|
|
1203
|
+
| Output type | `TypeError` | Raises | `Result.failure` |
|
|
1204
|
+
|
|
929
1205
|
#### Exception Handling in Pipelines
|
|
930
1206
|
|
|
931
1207
|
When using `.call!` with `organize` pipelines, the exception capture behavior is **chained** to all steps. This is especially useful with `on_failure: :collect`:
|
data/lib/senro_usecaser/base.rb
CHANGED
|
@@ -262,8 +262,9 @@ module SenroUsecaser
|
|
|
262
262
|
end
|
|
263
263
|
|
|
264
264
|
# Adds an around hook
|
|
265
|
+
# Block receives (input, use_case, &block) where use_case allows access to dependencies
|
|
265
266
|
#
|
|
266
|
-
#: () { (untyped) { () -> Result[untyped] } -> Result[untyped] } -> void
|
|
267
|
+
#: () { (untyped, Base) { () -> Result[untyped] } -> Result[untyped] } -> void
|
|
267
268
|
def around(&block)
|
|
268
269
|
around_hooks << block if block
|
|
269
270
|
end
|
|
@@ -275,17 +276,41 @@ module SenroUsecaser
|
|
|
275
276
|
@around_hooks ||= []
|
|
276
277
|
end
|
|
277
278
|
|
|
278
|
-
# Declares the expected input type for this UseCase
|
|
279
|
+
# Declares the expected input type(s) for this UseCase
|
|
280
|
+
# Accepts a Class or one or more Modules that input must include
|
|
279
281
|
#
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
282
|
+
# @example Single class
|
|
283
|
+
# input UserInput
|
|
284
|
+
#
|
|
285
|
+
# @example Single module (interface)
|
|
286
|
+
# input HasUserId
|
|
287
|
+
#
|
|
288
|
+
# @example Multiple modules (interfaces)
|
|
289
|
+
# input HasUserId, HasEmail
|
|
290
|
+
#
|
|
291
|
+
#: (*Module) -> void
|
|
292
|
+
def input(*types)
|
|
293
|
+
@input_types = types
|
|
283
294
|
end
|
|
284
295
|
|
|
285
|
-
# Returns the input
|
|
296
|
+
# Returns the input types as an array
|
|
286
297
|
#
|
|
287
|
-
#: () ->
|
|
288
|
-
|
|
298
|
+
#: () -> Array[Module]
|
|
299
|
+
def input_types
|
|
300
|
+
@input_types || []
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Returns the input class (for backwards compatibility)
|
|
304
|
+
# If a Class is specified, returns it. Otherwise returns the first type.
|
|
305
|
+
#
|
|
306
|
+
#: () -> Module?
|
|
307
|
+
def input_class
|
|
308
|
+
types = input_types
|
|
309
|
+
return nil if types.empty?
|
|
310
|
+
|
|
311
|
+
# Class があればそれを返す(単一 Class 指定の後方互換)
|
|
312
|
+
types.find { |t| t.is_a?(Class) } || types.first
|
|
313
|
+
end
|
|
289
314
|
|
|
290
315
|
# Declares the expected output type for this UseCase
|
|
291
316
|
#
|
|
@@ -339,7 +364,7 @@ module SenroUsecaser
|
|
|
339
364
|
subclass.instance_variable_set(:@use_case_namespace, @use_case_namespace)
|
|
340
365
|
subclass.instance_variable_set(:@organized_steps, @organized_steps&.dup)
|
|
341
366
|
subclass.instance_variable_set(:@on_failure_strategy, @on_failure_strategy)
|
|
342
|
-
subclass.instance_variable_set(:@
|
|
367
|
+
subclass.instance_variable_set(:@input_types, @input_types&.dup)
|
|
343
368
|
subclass.instance_variable_set(:@output_schema, @output_schema)
|
|
344
369
|
end
|
|
345
370
|
|
|
@@ -371,6 +396,8 @@ module SenroUsecaser
|
|
|
371
396
|
raise ArgumentError, "#{self.class.name} must define `input` class"
|
|
372
397
|
end
|
|
373
398
|
|
|
399
|
+
validate_input!(input)
|
|
400
|
+
|
|
374
401
|
execute_with_hooks(input) do
|
|
375
402
|
call(input)
|
|
376
403
|
end
|
|
@@ -415,6 +442,48 @@ module SenroUsecaser
|
|
|
415
442
|
Result.capture(*exception_classes, code: code, &)
|
|
416
443
|
end
|
|
417
444
|
|
|
445
|
+
# Validates that input satisfies all declared input types
|
|
446
|
+
# For Modules: checks if input's class includes the module
|
|
447
|
+
# For Classes: checks if input is an instance of the class
|
|
448
|
+
#
|
|
449
|
+
#: (untyped) -> void
|
|
450
|
+
def validate_input!(input)
|
|
451
|
+
types = self.class.input_types
|
|
452
|
+
return if types.empty?
|
|
453
|
+
|
|
454
|
+
types.each do |expected_type|
|
|
455
|
+
if expected_type.is_a?(Module) && !expected_type.is_a?(Class)
|
|
456
|
+
# Module の場合: include しているかを検査
|
|
457
|
+
unless input.class.include?(expected_type)
|
|
458
|
+
raise ArgumentError,
|
|
459
|
+
"Input #{input.class} must include #{expected_type}"
|
|
460
|
+
end
|
|
461
|
+
elsif !input.is_a?(expected_type)
|
|
462
|
+
# Class の場合: インスタンスかを検査
|
|
463
|
+
raise ArgumentError,
|
|
464
|
+
"Input must be an instance of #{expected_type}, got #{input.class}"
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Validates that the result's value satisfies the declared output type
|
|
470
|
+
# Only validates if result is success and output_schema is a Class
|
|
471
|
+
#
|
|
472
|
+
#: (Result[untyped]) -> void
|
|
473
|
+
def validate_output!(result)
|
|
474
|
+
return unless result.success?
|
|
475
|
+
|
|
476
|
+
expected_type = self.class.output_schema
|
|
477
|
+
return if expected_type.nil?
|
|
478
|
+
return unless expected_type.is_a?(Class)
|
|
479
|
+
|
|
480
|
+
value = result.value
|
|
481
|
+
return if value.is_a?(expected_type)
|
|
482
|
+
|
|
483
|
+
raise TypeError,
|
|
484
|
+
"Output must be an instance of #{expected_type}, got #{value.class}"
|
|
485
|
+
end
|
|
486
|
+
|
|
418
487
|
# Executes the core logic with before/after/around hooks
|
|
419
488
|
#
|
|
420
489
|
#: (untyped) { () -> Result[untyped] } -> Result[untyped]
|
|
@@ -422,6 +491,7 @@ module SenroUsecaser
|
|
|
422
491
|
execution = build_around_chain(input, core_block)
|
|
423
492
|
run_before_hooks(input)
|
|
424
493
|
result = execution.call
|
|
494
|
+
validate_output!(result)
|
|
425
495
|
run_after_hooks(input, result)
|
|
426
496
|
result
|
|
427
497
|
end
|
|
@@ -440,22 +510,39 @@ module SenroUsecaser
|
|
|
440
510
|
#: (untyped, Proc) -> Proc
|
|
441
511
|
def build_around_chain(input, core_block)
|
|
442
512
|
wrapped_core = -> { wrap_result(core_block.call) }
|
|
443
|
-
|
|
513
|
+
chain = wrap_extension_around_hooks(input, wrapped_core)
|
|
514
|
+
wrap_block_around_hooks(input, chain)
|
|
515
|
+
end
|
|
444
516
|
|
|
445
|
-
|
|
517
|
+
# Wraps extension/module around hooks
|
|
518
|
+
#
|
|
519
|
+
#: (untyped, Proc) -> Proc
|
|
520
|
+
def wrap_extension_around_hooks(input, chain)
|
|
521
|
+
collect_extension_around_hooks.reverse.reduce(chain) do |inner, hook|
|
|
446
522
|
-> { wrap_result(hook.call(input) { inner.call }) }
|
|
447
523
|
end
|
|
448
524
|
end
|
|
449
525
|
|
|
450
|
-
#
|
|
526
|
+
# Wraps block-based around hooks (pass self as second argument)
|
|
527
|
+
#
|
|
528
|
+
#: (untyped, Proc) -> Proc
|
|
529
|
+
def wrap_block_around_hooks(input, chain)
|
|
530
|
+
use_case_instance = self
|
|
531
|
+
self.class.around_hooks.reverse.reduce(chain) do |inner, hook|
|
|
532
|
+
-> { wrap_result(hook.call(input, use_case_instance) { inner.call }) }
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Collects around hooks from Hook classes and extension modules (not block-based)
|
|
451
537
|
#
|
|
452
538
|
#: () -> Array[Proc]
|
|
453
|
-
def
|
|
454
|
-
hooks =
|
|
539
|
+
def collect_extension_around_hooks
|
|
540
|
+
hooks = hook_instances.map { |hook_instance| hook_instance.method(:around).to_proc }
|
|
455
541
|
self.class.extensions.each do |ext|
|
|
542
|
+
next if hook_class?(ext)
|
|
543
|
+
|
|
456
544
|
hooks << ext.method(:around).to_proc if ext.respond_to?(:around)
|
|
457
545
|
end
|
|
458
|
-
hooks.concat(self.class.around_hooks)
|
|
459
546
|
hooks
|
|
460
547
|
end
|
|
461
548
|
|
|
@@ -463,20 +550,49 @@ module SenroUsecaser
|
|
|
463
550
|
#
|
|
464
551
|
#: (untyped) -> void
|
|
465
552
|
def run_before_hooks(input)
|
|
553
|
+
hook_instances.each do |hook_instance|
|
|
554
|
+
hook_instance.before(input)
|
|
555
|
+
end
|
|
466
556
|
self.class.extensions.each do |ext|
|
|
557
|
+
next if hook_class?(ext)
|
|
558
|
+
|
|
467
559
|
ext.send(:before, input) if ext.respond_to?(:before)
|
|
468
560
|
end
|
|
469
|
-
self.class.before_hooks.each { |hook|
|
|
561
|
+
self.class.before_hooks.each { |hook| instance_exec(input, &hook) } # steep:ignore BlockTypeMismatch
|
|
470
562
|
end
|
|
471
563
|
|
|
472
564
|
# Runs all after hooks
|
|
473
565
|
#
|
|
474
566
|
#: (untyped, Result[untyped]) -> void
|
|
475
567
|
def run_after_hooks(input, result)
|
|
568
|
+
hook_instances.each do |hook_instance|
|
|
569
|
+
hook_instance.after(input, result)
|
|
570
|
+
end
|
|
476
571
|
self.class.extensions.each do |ext|
|
|
572
|
+
next if hook_class?(ext)
|
|
573
|
+
|
|
477
574
|
ext.send(:after, input, result) if ext.respond_to?(:after)
|
|
478
575
|
end
|
|
479
|
-
self.class.after_hooks.each { |hook|
|
|
576
|
+
self.class.after_hooks.each { |hook| instance_exec(input, result, &hook) } # steep:ignore BlockTypeMismatch
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# Returns instantiated hook objects
|
|
580
|
+
#
|
|
581
|
+
#: () -> Array[Hook]
|
|
582
|
+
def hook_instances
|
|
583
|
+
@hook_instances ||= self.class.extensions.filter_map do |ext|
|
|
584
|
+
next unless hook_class?(ext)
|
|
585
|
+
|
|
586
|
+
hook_class = ext #: singleton(Hook)
|
|
587
|
+
hook_class.new(container: @_container, use_case_namespace: effective_namespace)
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
# Checks if the extension is a Hook class
|
|
592
|
+
#
|
|
593
|
+
#: (untyped) -> bool
|
|
594
|
+
def hook_class?(ext)
|
|
595
|
+
ext.is_a?(Class) && ext < Hook
|
|
480
596
|
end
|
|
481
597
|
|
|
482
598
|
# Resolves dependencies from the container
|
|
@@ -645,12 +761,12 @@ module SenroUsecaser
|
|
|
645
761
|
end
|
|
646
762
|
|
|
647
763
|
# Calls a single UseCase in the pipeline
|
|
648
|
-
# Requires
|
|
764
|
+
# Requires input type(s) to be defined for pipeline steps
|
|
649
765
|
#
|
|
650
766
|
#: (singleton(Base), untyped) -> Result[untyped]
|
|
651
767
|
def call_use_case(use_case_class, input)
|
|
652
|
-
|
|
653
|
-
raise ArgumentError, "#{use_case_class.name} must define `input`
|
|
768
|
+
if use_case_class.input_types.empty?
|
|
769
|
+
raise ArgumentError, "#{use_case_class.name} must define `input` type(s) to be used in a pipeline"
|
|
654
770
|
end
|
|
655
771
|
|
|
656
772
|
call_method = @_capture_exceptions || false ? :call! : :call #: Symbol
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
module SenroUsecaser
|
|
6
|
+
# Base class for hooks with dependency injection support
|
|
7
|
+
#
|
|
8
|
+
# Hook classes provide a way to define before/after/around hooks
|
|
9
|
+
# with access to the DI container and automatic dependency resolution.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic hook
|
|
12
|
+
# class LoggingHook < SenroUsecaser::Hook
|
|
13
|
+
# depends_on :logger, Logger
|
|
14
|
+
#
|
|
15
|
+
# def before(input)
|
|
16
|
+
# logger.info("Starting with #{input.class.name}")
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# def after(input, result)
|
|
20
|
+
# logger.info("Finished: #{result.success? ? 'success' : 'failure'}")
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# @example Hook with namespace
|
|
25
|
+
# class Admin::AuditHook < SenroUsecaser::Hook
|
|
26
|
+
# namespace :admin
|
|
27
|
+
# depends_on :audit_logger, AuditLogger
|
|
28
|
+
#
|
|
29
|
+
# def after(input, result)
|
|
30
|
+
# audit_logger.log(input: input, result: result)
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# @example Hook with around
|
|
35
|
+
# class TransactionHook < SenroUsecaser::Hook
|
|
36
|
+
# def around(input)
|
|
37
|
+
# ActiveRecord::Base.transaction { yield }
|
|
38
|
+
# end
|
|
39
|
+
# end
|
|
40
|
+
class Hook
|
|
41
|
+
class << self
|
|
42
|
+
# Declares a dependency to be injected from the container
|
|
43
|
+
#
|
|
44
|
+
#: (Symbol, ?Class) -> void
|
|
45
|
+
def depends_on(name, type = nil)
|
|
46
|
+
dependencies << name unless dependencies.include?(name)
|
|
47
|
+
dependency_types[name] = type if type
|
|
48
|
+
|
|
49
|
+
define_method(name) do
|
|
50
|
+
@_dependencies[name]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns the list of declared dependencies
|
|
55
|
+
#
|
|
56
|
+
#: () -> Array[Symbol]
|
|
57
|
+
def dependencies
|
|
58
|
+
@dependencies ||= []
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns the dependency type mapping
|
|
62
|
+
#
|
|
63
|
+
#: () -> Hash[Symbol, Class]
|
|
64
|
+
def dependency_types
|
|
65
|
+
@dependency_types ||= {}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Sets or returns the namespace for dependency resolution
|
|
69
|
+
#
|
|
70
|
+
#: (?(Symbol | String)) -> (Symbol | String)?
|
|
71
|
+
def namespace(name = nil)
|
|
72
|
+
if name
|
|
73
|
+
@hook_namespace = name
|
|
74
|
+
else
|
|
75
|
+
@hook_namespace
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Alias for namespace() without arguments
|
|
80
|
+
#
|
|
81
|
+
#: () -> (Symbol | String)?
|
|
82
|
+
def hook_namespace # rubocop:disable Style/TrivialAccessors
|
|
83
|
+
@hook_namespace
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @api private
|
|
87
|
+
def inherited(subclass)
|
|
88
|
+
super
|
|
89
|
+
subclass.instance_variable_set(:@dependencies, dependencies.dup)
|
|
90
|
+
subclass.instance_variable_set(:@dependency_types, dependency_types.dup)
|
|
91
|
+
subclass.instance_variable_set(:@hook_namespace, @hook_namespace)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Initializes the hook with dependencies resolved from the container
|
|
96
|
+
#
|
|
97
|
+
#: (container: Container, ?use_case_namespace: (Symbol | String)?) -> void
|
|
98
|
+
def initialize(container:, use_case_namespace: nil)
|
|
99
|
+
@_container = container
|
|
100
|
+
@_use_case_namespace = use_case_namespace
|
|
101
|
+
@_dependencies = {} #: Hash[Symbol, untyped]
|
|
102
|
+
|
|
103
|
+
resolve_dependencies
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Called before the UseCase executes
|
|
107
|
+
# Override in subclass to add before logic
|
|
108
|
+
#
|
|
109
|
+
#: (untyped) -> void
|
|
110
|
+
def before(input)
|
|
111
|
+
# Override in subclass
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Called after the UseCase executes
|
|
115
|
+
# Override in subclass to add after logic
|
|
116
|
+
#
|
|
117
|
+
#: (untyped, Result[untyped]) -> void
|
|
118
|
+
def after(input, result)
|
|
119
|
+
# Override in subclass
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Wraps the UseCase execution
|
|
123
|
+
# Override in subclass to add around logic
|
|
124
|
+
#
|
|
125
|
+
#: (untyped) { () -> Result[untyped] } -> Result[untyped]
|
|
126
|
+
def around(_input)
|
|
127
|
+
yield
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
# Returns the effective namespace for dependency resolution
|
|
133
|
+
#
|
|
134
|
+
#: () -> (Symbol | String)?
|
|
135
|
+
def effective_namespace
|
|
136
|
+
return self.class.hook_namespace if self.class.hook_namespace
|
|
137
|
+
return @_use_case_namespace if @_use_case_namespace
|
|
138
|
+
return nil unless SenroUsecaser.configuration.infer_namespace_from_module
|
|
139
|
+
|
|
140
|
+
infer_namespace_from_class
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Infers namespace from the class's module structure
|
|
144
|
+
#
|
|
145
|
+
#: () -> String?
|
|
146
|
+
def infer_namespace_from_class
|
|
147
|
+
class_name = self.class.name
|
|
148
|
+
return nil unless class_name
|
|
149
|
+
|
|
150
|
+
parts = class_name.split("::")
|
|
151
|
+
return nil if parts.length <= 1
|
|
152
|
+
|
|
153
|
+
module_parts = parts[0...-1] || [] #: Array[String]
|
|
154
|
+
return nil if module_parts.empty?
|
|
155
|
+
|
|
156
|
+
module_parts.map { |part| part.gsub(/([a-z])([A-Z])/, '\1_\2').downcase }.join("::")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Resolves dependencies from the container
|
|
160
|
+
#
|
|
161
|
+
#: () -> void
|
|
162
|
+
def resolve_dependencies
|
|
163
|
+
self.class.dependencies.each do |name|
|
|
164
|
+
@_dependencies[name] = resolve_from_container(name)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Resolves a single dependency from the container
|
|
169
|
+
#
|
|
170
|
+
#: (Symbol) -> untyped
|
|
171
|
+
def resolve_from_container(name)
|
|
172
|
+
namespace = effective_namespace
|
|
173
|
+
if namespace
|
|
174
|
+
@_container.resolve_in(namespace, name)
|
|
175
|
+
else
|
|
176
|
+
@_container.resolve(name)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
data/lib/senro_usecaser.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "senro_usecaser/result"
|
|
|
8
8
|
require_relative "senro_usecaser/container"
|
|
9
9
|
require_relative "senro_usecaser/configuration"
|
|
10
10
|
require_relative "senro_usecaser/provider"
|
|
11
|
+
require_relative "senro_usecaser/hook"
|
|
11
12
|
require_relative "senro_usecaser/base"
|
|
12
13
|
|
|
13
14
|
# SenroUsecaser is a type-safe UseCase pattern implementation library for Ruby.
|
|
@@ -178,8 +178,9 @@ module SenroUsecaser
|
|
|
178
178
|
def self.after_hooks: () -> Array[Proc]
|
|
179
179
|
|
|
180
180
|
# Adds an around hook
|
|
181
|
+
# Block receives (input, use_case, &block) where use_case allows access to dependencies
|
|
181
182
|
#
|
|
182
|
-
# : () { (untyped) { () -> Result[untyped] } -> Result[untyped] } -> void
|
|
183
|
+
# : () { (untyped, Base) { () -> Result[untyped] } -> Result[untyped] } -> void
|
|
183
184
|
def self.around: () ?{ (?) -> untyped } -> untyped
|
|
184
185
|
|
|
185
186
|
# Returns the list of around hooks
|
|
@@ -187,15 +188,31 @@ module SenroUsecaser
|
|
|
187
188
|
# : () -> Array[Proc]
|
|
188
189
|
def self.around_hooks: () -> Array[Proc]
|
|
189
190
|
|
|
190
|
-
# Declares the expected input type for this UseCase
|
|
191
|
+
# Declares the expected input type(s) for this UseCase
|
|
192
|
+
# Accepts a Class or one or more Modules that input must include
|
|
191
193
|
#
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
+
# @example Single class
|
|
195
|
+
# input UserInput
|
|
196
|
+
#
|
|
197
|
+
# @example Single module (interface)
|
|
198
|
+
# input HasUserId
|
|
199
|
+
#
|
|
200
|
+
# @example Multiple modules (interfaces)
|
|
201
|
+
# input HasUserId, HasEmail
|
|
202
|
+
#
|
|
203
|
+
# : (*Module) -> void
|
|
204
|
+
def self.input: (*Module) -> void
|
|
194
205
|
|
|
195
|
-
# Returns the input
|
|
206
|
+
# Returns the input types as an array
|
|
196
207
|
#
|
|
197
|
-
# : () ->
|
|
198
|
-
|
|
208
|
+
# : () -> Array[Module]
|
|
209
|
+
def self.input_types: () -> Array[Module]
|
|
210
|
+
|
|
211
|
+
# Returns the input class (for backwards compatibility)
|
|
212
|
+
# If a Class is specified, returns it. Otherwise returns the first type.
|
|
213
|
+
#
|
|
214
|
+
# : () -> Module?
|
|
215
|
+
def self.input_class: () -> Module?
|
|
199
216
|
|
|
200
217
|
# Declares the expected output type for this UseCase
|
|
201
218
|
#
|
|
@@ -266,6 +283,19 @@ module SenroUsecaser
|
|
|
266
283
|
# : [T] (*Class, ?code: Symbol) { () -> T } -> Result[T]
|
|
267
284
|
def capture: [T] (*Class, ?code: Symbol) { () -> T } -> Result[T]
|
|
268
285
|
|
|
286
|
+
# Validates that input satisfies all declared input types
|
|
287
|
+
# For Modules: checks if input's class includes the module
|
|
288
|
+
# For Classes: checks if input is an instance of the class
|
|
289
|
+
#
|
|
290
|
+
# : (untyped) -> void
|
|
291
|
+
def validate_input!: (untyped) -> void
|
|
292
|
+
|
|
293
|
+
# Validates that the result's value satisfies the declared output type
|
|
294
|
+
# Only validates if result is success and output_schema is a Class
|
|
295
|
+
#
|
|
296
|
+
# : (Result[untyped]) -> void
|
|
297
|
+
def validate_output!: (Result[untyped]) -> void
|
|
298
|
+
|
|
269
299
|
# Executes the core logic with before/after/around hooks
|
|
270
300
|
#
|
|
271
301
|
# : (untyped) { () -> Result[untyped] } -> Result[untyped]
|
|
@@ -281,10 +311,20 @@ module SenroUsecaser
|
|
|
281
311
|
# : (untyped, Proc) -> Proc
|
|
282
312
|
def build_around_chain: (untyped, Proc) -> Proc
|
|
283
313
|
|
|
284
|
-
#
|
|
314
|
+
# Wraps extension/module around hooks
|
|
315
|
+
#
|
|
316
|
+
# : (untyped, Proc) -> Proc
|
|
317
|
+
def wrap_extension_around_hooks: (untyped, Proc) -> Proc
|
|
318
|
+
|
|
319
|
+
# Wraps block-based around hooks (pass self as second argument)
|
|
320
|
+
#
|
|
321
|
+
# : (untyped, Proc) -> Proc
|
|
322
|
+
def wrap_block_around_hooks: (untyped, Proc) -> Proc
|
|
323
|
+
|
|
324
|
+
# Collects around hooks from Hook classes and extension modules (not block-based)
|
|
285
325
|
#
|
|
286
326
|
# : () -> Array[Proc]
|
|
287
|
-
def
|
|
327
|
+
def collect_extension_around_hooks: () -> Array[Proc]
|
|
288
328
|
|
|
289
329
|
# Runs all before hooks
|
|
290
330
|
#
|
|
@@ -296,6 +336,16 @@ module SenroUsecaser
|
|
|
296
336
|
# : (untyped, Result[untyped]) -> void
|
|
297
337
|
def run_after_hooks: (untyped, Result[untyped]) -> void
|
|
298
338
|
|
|
339
|
+
# Returns instantiated hook objects
|
|
340
|
+
#
|
|
341
|
+
# : () -> Array[Hook]
|
|
342
|
+
def hook_instances: () -> Array[Hook]
|
|
343
|
+
|
|
344
|
+
# Checks if the extension is a Hook class
|
|
345
|
+
#
|
|
346
|
+
# : (untyped) -> bool
|
|
347
|
+
def hook_class?: (untyped) -> bool
|
|
348
|
+
|
|
299
349
|
# Resolves dependencies from the container
|
|
300
350
|
#
|
|
301
351
|
# : (Container, Hash[Symbol, untyped]) -> void
|
|
@@ -357,7 +407,7 @@ module SenroUsecaser
|
|
|
357
407
|
def step_should_stop?: (Step) -> bool
|
|
358
408
|
|
|
359
409
|
# Calls a single UseCase in the pipeline
|
|
360
|
-
# Requires
|
|
410
|
+
# Requires input type(s) to be defined for pipeline steps
|
|
361
411
|
#
|
|
362
412
|
# : (singleton(Base), untyped) -> Result[untyped]
|
|
363
413
|
def call_use_case: (singleton(Base), untyped) -> Result[untyped]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Generated from lib/senro_usecaser/hook.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SenroUsecaser
|
|
4
|
+
# Base class for hooks with dependency injection support
|
|
5
|
+
#
|
|
6
|
+
# Hook classes provide a way to define before/after/around hooks
|
|
7
|
+
# with access to the DI container and automatic dependency resolution.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic hook
|
|
10
|
+
# class LoggingHook < SenroUsecaser::Hook
|
|
11
|
+
# depends_on :logger, Logger
|
|
12
|
+
#
|
|
13
|
+
# def before(input)
|
|
14
|
+
# logger.info("Starting with #{input.class.name}")
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# def after(input, result)
|
|
18
|
+
# logger.info("Finished: #{result.success? ? 'success' : 'failure'}")
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# @example Hook with namespace
|
|
23
|
+
# class Admin::AuditHook < SenroUsecaser::Hook
|
|
24
|
+
# namespace :admin
|
|
25
|
+
# depends_on :audit_logger, AuditLogger
|
|
26
|
+
#
|
|
27
|
+
# def after(input, result)
|
|
28
|
+
# audit_logger.log(input: input, result: result)
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# @example Hook with around
|
|
33
|
+
# class TransactionHook < SenroUsecaser::Hook
|
|
34
|
+
# def around(input)
|
|
35
|
+
# ActiveRecord::Base.transaction { yield }
|
|
36
|
+
# end
|
|
37
|
+
# end
|
|
38
|
+
class Hook
|
|
39
|
+
# Declares a dependency to be injected from the container
|
|
40
|
+
#
|
|
41
|
+
# : (Symbol, ?Class) -> void
|
|
42
|
+
def self.depends_on: (Symbol, ?Class) -> void
|
|
43
|
+
|
|
44
|
+
# Returns the list of declared dependencies
|
|
45
|
+
#
|
|
46
|
+
# : () -> Array[Symbol]
|
|
47
|
+
def self.dependencies: () -> Array[Symbol]
|
|
48
|
+
|
|
49
|
+
# Returns the dependency type mapping
|
|
50
|
+
#
|
|
51
|
+
# : () -> Hash[Symbol, Class]
|
|
52
|
+
def self.dependency_types: () -> Hash[Symbol, Class]
|
|
53
|
+
|
|
54
|
+
# Sets or returns the namespace for dependency resolution
|
|
55
|
+
#
|
|
56
|
+
# : (?(Symbol | String)) -> (Symbol | String)?
|
|
57
|
+
def self.namespace: (?Symbol | String) -> (Symbol | String)?
|
|
58
|
+
|
|
59
|
+
# Alias for namespace() without arguments
|
|
60
|
+
#
|
|
61
|
+
# : () -> (Symbol | String)?
|
|
62
|
+
def self.hook_namespace: () -> (Symbol | String)?
|
|
63
|
+
|
|
64
|
+
# @api private
|
|
65
|
+
def self.inherited: (untyped subclass) -> untyped
|
|
66
|
+
|
|
67
|
+
# Initializes the hook with dependencies resolved from the container
|
|
68
|
+
#
|
|
69
|
+
# : (container: Container, ?use_case_namespace: (Symbol | String)?) -> void
|
|
70
|
+
def initialize: (container: Container, ?use_case_namespace: (Symbol | String)?) -> void
|
|
71
|
+
|
|
72
|
+
# Called before the UseCase executes
|
|
73
|
+
# Override in subclass to add before logic
|
|
74
|
+
#
|
|
75
|
+
# : (untyped) -> void
|
|
76
|
+
def before: (untyped) -> void
|
|
77
|
+
|
|
78
|
+
# Called after the UseCase executes
|
|
79
|
+
# Override in subclass to add after logic
|
|
80
|
+
#
|
|
81
|
+
# : (untyped, Result[untyped]) -> void
|
|
82
|
+
def after: (untyped, Result[untyped]) -> void
|
|
83
|
+
|
|
84
|
+
# Wraps the UseCase execution
|
|
85
|
+
# Override in subclass to add around logic
|
|
86
|
+
#
|
|
87
|
+
# : (untyped) { () -> Result[untyped] } -> Result[untyped]
|
|
88
|
+
def around: (untyped) { () -> Result[untyped] } -> Result[untyped]
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# Returns the effective namespace for dependency resolution
|
|
93
|
+
#
|
|
94
|
+
# : () -> (Symbol | String)?
|
|
95
|
+
def effective_namespace: () -> (Symbol | String)?
|
|
96
|
+
|
|
97
|
+
# Infers namespace from the class's module structure
|
|
98
|
+
#
|
|
99
|
+
# : () -> String?
|
|
100
|
+
def infer_namespace_from_class: () -> String?
|
|
101
|
+
|
|
102
|
+
# Resolves dependencies from the container
|
|
103
|
+
#
|
|
104
|
+
# : () -> void
|
|
105
|
+
def resolve_dependencies: () -> void
|
|
106
|
+
|
|
107
|
+
# Resolves a single dependency from the container
|
|
108
|
+
#
|
|
109
|
+
# : (Symbol) -> untyped
|
|
110
|
+
def resolve_from_container: (Symbol) -> untyped
|
|
111
|
+
end
|
|
112
|
+
end
|
data/sig/overrides.rbs
CHANGED
|
@@ -11,6 +11,6 @@ module SenroUsecaser
|
|
|
11
11
|
# Class methods that rbs-inline doesn't generate correctly
|
|
12
12
|
def self.organized_steps: () -> Array[Step]?
|
|
13
13
|
def self.use_case_namespace: () -> (Symbol | String)?
|
|
14
|
-
def self.
|
|
14
|
+
def self.output_schema: () -> (Class | Hash[Symbol, Class])?
|
|
15
15
|
end
|
|
16
16
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: senro_usecaser
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shogo Kawahara
|
|
@@ -22,6 +22,7 @@ extra_rdoc_files: []
|
|
|
22
22
|
files:
|
|
23
23
|
- ".rspec"
|
|
24
24
|
- ".rubocop.yml"
|
|
25
|
+
- CHANGELOG.md
|
|
25
26
|
- LICENSE
|
|
26
27
|
- README.md
|
|
27
28
|
- Rakefile
|
|
@@ -37,6 +38,7 @@ files:
|
|
|
37
38
|
- lib/senro_usecaser/configuration.rb
|
|
38
39
|
- lib/senro_usecaser/container.rb
|
|
39
40
|
- lib/senro_usecaser/error.rb
|
|
41
|
+
- lib/senro_usecaser/hook.rb
|
|
40
42
|
- lib/senro_usecaser/provider.rb
|
|
41
43
|
- lib/senro_usecaser/result.rb
|
|
42
44
|
- lib/senro_usecaser/version.rb
|
|
@@ -45,6 +47,7 @@ files:
|
|
|
45
47
|
- sig/generated/senro_usecaser/configuration.rbs
|
|
46
48
|
- sig/generated/senro_usecaser/container.rbs
|
|
47
49
|
- sig/generated/senro_usecaser/error.rbs
|
|
50
|
+
- sig/generated/senro_usecaser/hook.rbs
|
|
48
51
|
- sig/generated/senro_usecaser/provider.rbs
|
|
49
52
|
- sig/generated/senro_usecaser/result.rbs
|
|
50
53
|
- sig/generated/senro_usecaser/version.rbs
|