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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +72 -0
- data/LICENSE +21 -0
- data/README.md +1069 -0
- data/Rakefile +12 -0
- data/Steepfile +24 -0
- data/examples/RBS_GENERATION.md +16 -0
- data/examples/namespace_demo.rb +751 -0
- data/examples/order_system.rb +1279 -0
- data/examples/sig/namespace_demo.rbs +279 -0
- data/examples/sig/order_system.rbs +685 -0
- data/lefthook.yml +31 -0
- data/lib/senro_usecaser/base.rb +660 -0
- data/lib/senro_usecaser/configuration.rb +149 -0
- data/lib/senro_usecaser/container.rb +315 -0
- data/lib/senro_usecaser/error.rb +88 -0
- data/lib/senro_usecaser/provider.rb +212 -0
- data/lib/senro_usecaser/result.rb +182 -0
- data/lib/senro_usecaser/version.rb +8 -0
- data/lib/senro_usecaser.rb +155 -0
- data/sig/generated/senro_usecaser/base.rbs +365 -0
- data/sig/generated/senro_usecaser/configuration.rbs +80 -0
- data/sig/generated/senro_usecaser/container.rbs +190 -0
- data/sig/generated/senro_usecaser/error.rbs +58 -0
- data/sig/generated/senro_usecaser/provider.rbs +153 -0
- data/sig/generated/senro_usecaser/result.rbs +109 -0
- data/sig/generated/senro_usecaser/version.rbs +6 -0
- data/sig/generated/senro_usecaser.rbs +113 -0
- data/sig/overrides.rbs +16 -0
- metadata +77 -0
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
|