Skip to content

Commit 1fe82de

Browse files
committed
Merge #161: feat: [#156] Implement Dispatch Layer with ExecutionContext wrapper
aa531e3 feat: [#156] implement dispatch layer with ExecutionContext wrapper (Jose Celano) 04d0d5f feat: [#156] implement Dispatch Layer with ExecutionContext wrapper (Jose Celano) Pull request description: ## 🎯 Overview This PR implements **Proposal 2: Create Dispatch Layer** from the presentation layer reorganization epic ([#154](#154)). The Dispatch Layer establishes clear separation between command routing, execution context, and command execution, following the four-layer presentation architecture: **Input → Dispatch → Controllers → Views**. ## 📋 What's Implemented ### Core Components - **ExecutionContext wrapper** around `Arc<Container>` for future-proof command signatures - **Central command routing** with `route_command(command, working_dir, &context)` function - **Complete dispatch module** with router and context submodules - **Bootstrap integration** - Updated `bootstrap/app.rs` to use new dispatch layer - **Legacy deprecation** - Marked old `presentation::execute` as deprecated with migration guide ### Documentation & Quality - **Architectural Decision Record** - [`docs/decisions/execution-context-wrapper.md`](docs/decisions/execution-context-wrapper.md) documenting design rationale - **Comprehensive documentation** - Module-level docs explaining architecture and benefits - **Fixed all doctest issues** - All 303 documentation tests compile and pass - **Linting fixes** - Added 'configurator' to spell-check dictionary, fixed clippy issues ## 🏗️ Architecture Benefits ### ExecutionContext Design The dispatch layer uses an `ExecutionContext` wrapper instead of passing `Container` directly: ```rust pub struct ExecutionContext { container: Arc<Container>, // Future extensions without breaking changes: // request_id: RequestId, // execution_metadata: ExecutionMetadata, // tracing_context: TracingContext, } ``` **Benefits:** - **Future-proof** - Can extend execution context without breaking command signatures - **Clear abstraction** - Represents "everything a command needs" vs generic container - **Command-specific access** - Convenience methods for common services - **Type safety** - Compile-time guarantees for service access ### Routing Architecture ```rust pub fn route_command( command: Commands, working_dir: &Path, context: &ExecutionContext, ) -> Result<(), CommandError> ``` **Benefits:** - **Centralized routing** - All command dispatch logic in one place - **Type safety** - Compile-time guarantees that all commands are handled - **Testable** - Router can be tested independently of handlers - **Scalable** - Easy to add new commands without modifying existing code ## 🔧 Module Structure ```text src/presentation/dispatch/ ├── mod.rs # Layer exports and comprehensive documentation ├── router.rs # Central command routing logic └── context.rs # ExecutionContext wrapper implementation ``` ## ✅ Quality Validation All quality checks pass: - **1,200+ Unit Tests** ✅ - All pass successfully - **E2E Tests** ✅ - Provision and configuration tests validated - **Documentation Tests** ✅ - All 303 doctests compile and pass - **Linting** ✅ - cargo machete, clippy, rustfmt, shellcheck, cspell all pass - **Pre-commit Validation** ✅ - Complete pre-commit suite passes ## 📖 Related Documentation - **ADR**: [`docs/decisions/execution-context-wrapper.md`](docs/decisions/execution-context-wrapper.md) - Design rationale for ExecutionContext wrapper - **Epic Issue**: [#154](#154) - Presentation layer reorganization roadmap - **Module Documentation** - Comprehensive docs in `src/presentation/dispatch/mod.rs` ## 🚀 Next Steps After this PR is merged: 1. **Update epic progress** - Mark Proposal 2 as completed in [#156](#156) 2. **Begin Proposal 3** - Start implementing Controllers layer 3. **Integration** - New dispatch layer is ready for remaining presentation components ## 🔍 Migration Guide For code using the old `presentation::execute` function: ```rust // Old (deprecated) use torrust_tracker_deployer_lib::presentation::execute; execute(command, working_dir, user_output).await?; // New (recommended) use torrust_tracker_deployer_lib::presentation::dispatch::{route_command, ExecutionContext}; let context = ExecutionContext::new(Arc::new(container)); route_command(command, working_dir, &context)?; ``` --- **Closes:** #156 **Part of Epic:** #154 ACKs for top commit: josecelano: ACK aa531e3 Tree-SHA512: c7a2815acef975d06a620943223a68e34f0463d81621cea96dc3c5a072d43fe6b08da33d5d540cc62cd24ec3f78431260bc85691af6c8dba9bec82081b49270a
2 parents 0267458 + aa531e3 commit 1fe82de

File tree

9 files changed

+726
-4
lines changed

9 files changed

+726
-4
lines changed

docs/decisions/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ This directory contains architectural decision records for the Torrust Tracker D
66

77
| Status | Date | Decision | Summary |
88
| ----------- | ---------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
9+
| ✅ Accepted | 2025-11-07 | [ExecutionContext Wrapper Pattern](./execution-context-wrapper.md) | Use ExecutionContext wrapper around Container for future-proof command signatures |
910
| ✅ Accepted | 2025-11-03 | [Environment Variable Prefix](./environment-variable-prefix.md) | Use `TORRUST_TD_` prefix for all environment variables |
1011
| ✅ Accepted | 2025-10-15 | [External Tool Adapters Organization](./external-tool-adapters-organization.md) | Consolidate external tool wrappers in `src/adapters/` for better discoverability |
1112
| ✅ Accepted | 2025-10-10 | [Repository Rename to Deployer](./repository-rename-to-deployer.md) | Rename from "Torrust Tracker Deploy" to "Torrust Tracker Deployer" for production use |
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
# Decision: ExecutionContext Wrapper Pattern
2+
3+
## Status
4+
5+
Accepted
6+
7+
## Date
8+
9+
2025-11-07
10+
11+
## Context
12+
13+
During the implementation of the Dispatch Layer (Proposal 2 from the presentation layer reorganization epic #154), we needed to decide how command handlers should access application services. We had two main options:
14+
15+
1. **Direct Container Access**: Pass the `Container` directly to command handlers
16+
2. **ExecutionContext Wrapper**: Create an `ExecutionContext` wrapper around the `Container`
17+
18+
### Current Architecture
19+
20+
The Dispatch Layer routes commands to handlers and needs to provide access to application services (user output, repositories, external tool clients, etc.). These services are managed by the dependency injection `Container`.
21+
22+
### Design Options Considered
23+
24+
#### Option 1: Direct Container Access
25+
26+
```rust
27+
pub fn route_command(
28+
command: Commands,
29+
working_dir: &Path,
30+
container: &Container,
31+
) -> Result<(), CommandError>
32+
33+
// In handlers:
34+
fn handle_create_command(container: &Container) {
35+
let user_output = container.user_output();
36+
// ...
37+
}
38+
```
39+
40+
#### Option 2: ExecutionContext Wrapper
41+
42+
```rust
43+
pub struct ExecutionContext {
44+
container: Arc<Container>,
45+
}
46+
47+
pub fn route_command(
48+
command: Commands,
49+
working_dir: &Path,
50+
context: &ExecutionContext,
51+
) -> Result<(), CommandError>
52+
53+
// In handlers:
54+
fn handle_create_command(context: &ExecutionContext) {
55+
let user_output = context.user_output();
56+
// ...
57+
}
58+
```
59+
60+
## Decision
61+
62+
We chose **Option 2: ExecutionContext Wrapper** for the following reasons:
63+
64+
### 1. Future-Proof Command Signatures
65+
66+
By introducing `ExecutionContext`, we can add execution-related data in the future without breaking existing command handler signatures:
67+
68+
```rust
69+
pub struct ExecutionContext {
70+
container: Arc<Container>,
71+
// Future additions without breaking changes:
72+
// request_id: RequestId,
73+
// execution_metadata: ExecutionMetadata,
74+
// tracing_context: TracingContext,
75+
// user_permissions: UserPermissions,
76+
// execution_timeout: Duration,
77+
}
78+
```
79+
80+
If we used `Container` directly, adding any execution context would require changing every command handler signature.
81+
82+
### 2. Clear Abstraction and Intent
83+
84+
`ExecutionContext` provides a logical abstraction for "everything a command needs to execute" rather than exposing the dependency injection container directly:
85+
86+
- **Container**: Implementation detail for dependency injection
87+
- **ExecutionContext**: Execution abstraction for command handlers
88+
89+
This makes the intent clearer and separates concerns properly.
90+
91+
### 3. Type Safety and Interface Clarity
92+
93+
```rust
94+
// Less clear: What is this container for? Bootstrapping? Testing? Execution?
95+
fn handle_command(container: &Container)
96+
97+
// Clear: This is specifically for command execution
98+
fn handle_command(context: &ExecutionContext)
99+
```
100+
101+
### 4. Command-Specific Service Aggregation
102+
103+
ExecutionContext can provide command-specific convenience methods and service aggregations:
104+
105+
```rust
106+
impl ExecutionContext {
107+
// Direct service access
108+
pub fn user_output(&self) -> &Arc<Mutex<UserOutput>> {
109+
self.container.user_output()
110+
}
111+
112+
// Future: Command-specific aggregated services
113+
pub fn deployment_services(&self) -> DeploymentServices {
114+
DeploymentServices {
115+
provisioner: self.container.provisioner(),
116+
configurator: self.container.configurator(),
117+
validator: self.container.validator(),
118+
}
119+
}
120+
}
121+
```
122+
123+
### 5. Enhanced Testability
124+
125+
Different execution contexts can be created for different scenarios:
126+
127+
```rust
128+
// Production context
129+
let context = ExecutionContext::new(container);
130+
131+
// Test context with mocks
132+
let context = TestExecutionContext::new(mock_container);
133+
134+
// Both can implement the same interface
135+
trait ExecutionContextTrait {
136+
fn user_output(&self) -> &Arc<Mutex<UserOutput>>;
137+
}
138+
```
139+
140+
### 6. Industry Pattern Alignment
141+
142+
Most frameworks use execution context patterns:
143+
144+
- **Spring Framework**: `ApplicationContext`
145+
- **ASP.NET Core**: `HttpContext`
146+
- **Express.js**: Request/Response context
147+
- **Go**: `context.Context`
148+
149+
This aligns with established patterns for managing execution state.
150+
151+
## Consequences
152+
153+
### Positive
154+
155+
- **Future-Proof**: Can extend execution context without breaking command signatures
156+
- **Clear Intent**: ExecutionContext clearly indicates its purpose for command execution
157+
- **Better Abstraction**: Separates execution concerns from dependency injection mechanics
158+
- **Enhanced Testability**: Enables different contexts for different testing scenarios
159+
- **Industry Alignment**: Follows established patterns from major frameworks
160+
161+
### Negative
162+
163+
- **Initial Overhead**: Currently just a thin wrapper around Container
164+
- **Additional Indirection**: One extra layer between commands and services
165+
- **Learning Curve**: New developers need to understand the wrapper pattern
166+
167+
### Migration Path
168+
169+
If needed, migration from Container to ExecutionContext (or vice versa) is straightforward:
170+
171+
```rust
172+
// From Container to ExecutionContext
173+
fn handle_command(container: &Container) -> fn handle_command(context: &ExecutionContext)
174+
175+
// From ExecutionContext to Container
176+
fn handle_command(context: &ExecutionContext) -> fn handle_command(container: &Container)
177+
```
178+
179+
## Implementation Details
180+
181+
### Current Implementation
182+
183+
```rust
184+
pub struct ExecutionContext {
185+
container: Arc<Container>,
186+
}
187+
188+
impl ExecutionContext {
189+
pub fn new(container: Arc<Container>) -> Self {
190+
Self { container }
191+
}
192+
193+
pub fn container(&self) -> &Container {
194+
&self.container
195+
}
196+
197+
pub fn user_output(&self) -> &Arc<Mutex<UserOutput>> {
198+
self.container.user_output()
199+
}
200+
}
201+
```
202+
203+
### Usage Pattern
204+
205+
```rust
206+
// In bootstrap/app.rs
207+
let container = Arc::new(Container::new());
208+
let context = ExecutionContext::new(container);
209+
210+
// In dispatch layer
211+
route_command(command, working_dir, &context)?;
212+
213+
// In command handlers
214+
fn handle_create_command(context: &ExecutionContext) {
215+
let user_output = context.user_output();
216+
// Command implementation
217+
}
218+
```
219+
220+
## Related Decisions
221+
222+
- [Presentation Layer Reorganization](../decisions/README.md) - Overall context for the four-layer architecture
223+
- [Command State Return Pattern](./command-state-return-pattern.md) - How commands return typed states
224+
225+
## References
226+
227+
- [Epic #154: Presentation Layer Reorganization](https://github.com/torrust/torrust-tracker-deployer/issues/154)
228+
- [Issue #156: Create Dispatch Layer](https://github.com/torrust/torrust-tracker-deployer/issues/156)
229+
- [Spring Framework ApplicationContext](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-introduction)
230+
- [ASP.NET Core HttpContext](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-context)
231+
- [Go Context Package](https://pkg.go.dev/context)

project-words.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ cloudinit
2323
Cockburn
2424
concepsts
2525
connrefused
26+
configurator
2627
containerd
2728
cpus
2829
creds

src/bootstrap/app.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
//! - **Single Responsibility**: Focus only on application bootstrap concerns
1818
//! - **Clean Separation**: No CLI parsing or business logic in this module
1919
20+
use std::sync::Arc;
21+
2022
use clap::Parser;
2123
use tracing::info;
2224

@@ -56,14 +58,15 @@ pub fn run() {
5658
);
5759

5860
// Initialize service container for dependency injection
59-
let container = bootstrap::Container::new();
61+
let container = Arc::new(bootstrap::Container::new());
62+
let context = presentation::dispatch::ExecutionContext::new(container);
6063

6164
match cli.command {
6265
Some(command) => {
6366
if let Err(e) =
64-
presentation::execute(command, &cli.global.working_dir, &container.user_output())
67+
presentation::dispatch::route_command(command, &cli.global.working_dir, &context)
6568
{
66-
presentation::handle_error(&e, &container.user_output());
69+
presentation::handle_error(&e, &context.user_output());
6770
std::process::exit(1);
6871
}
6972
}

src/presentation/commands/mod.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,34 @@ pub mod tests;
2828

2929
/// Execute the given command
3030
///
31+
/// **DEPRECATED**: This function is deprecated in favor of the new Dispatch Layer.
32+
/// Use `crate::presentation::dispatch::route_command` instead.
33+
///
34+
/// This function will be removed in a future version. The new dispatch layer
35+
/// provides better separation of concerns and cleaner architecture.
36+
///
37+
/// # Migration Guide
38+
///
39+
/// Old code:
40+
/// ```rust,ignore
41+
/// use std::sync::{Arc, Mutex};
42+
/// use crate::presentation::{commands, user_output::UserOutput};
43+
///
44+
/// let user_output = Arc::new(Mutex::new(UserOutput::new(/* ... */)));
45+
/// commands::execute(command, working_dir, &user_output)?;
46+
/// ```
47+
///
48+
/// New code:
49+
/// ```rust,ignore
50+
/// use std::sync::Arc;
51+
/// use crate::bootstrap::Container;
52+
/// use crate::presentation::dispatch::{route_command, ExecutionContext};
53+
///
54+
/// let container = Arc::new(Container::new());
55+
/// let context = ExecutionContext::new(container);
56+
/// route_command(command, working_dir, &context)?;
57+
/// ```
58+
///
3159
/// This function serves as the central dispatcher for all CLI commands.
3260
/// It matches the command type and delegates execution to the appropriate
3361
/// command handler module.
@@ -65,6 +93,27 @@ pub mod tests;
6593
/// }
6694
/// }
6795
/// ```
96+
#[deprecated(
97+
since = "0.1.0",
98+
note = "Use `crate::presentation::dispatch::route_command` instead"
99+
)]
100+
///
101+
/// ```rust
102+
/// use clap::Parser;
103+
/// use torrust_tracker_deployer_lib::presentation::{input::cli, commands, user_output};
104+
/// use std::{path::Path, sync::{Arc, Mutex}};
105+
///
106+
/// let cli = cli::Cli::parse();
107+
/// if let Some(command) = cli.command {
108+
/// let working_dir = Path::new(".");
109+
/// let user_output = Arc::new(Mutex::new(user_output::UserOutput::new(user_output::VerbosityLevel::Normal)));
110+
/// let result = commands::execute(command, working_dir, &user_output);
111+
/// match result {
112+
/// Ok(_) => println!("Command executed successfully"),
113+
/// Err(e) => commands::handle_error(&e, &user_output),
114+
/// }
115+
/// }
116+
/// ```
68117
pub fn execute(
69118
command: Commands,
70119
working_dir: &std::path::Path,

0 commit comments

Comments
 (0)