Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 34 additions & 15 deletions src/presentation/controllers/release/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ use tracing::info;
use crate::application::command_handlers::release::ReleaseCommandHandler;
use crate::domain::environment::name::EnvironmentName;
use crate::domain::environment::repository::EnvironmentRepository;
use crate::domain::environment::state::Released;
use crate::domain::environment::Environment;
use crate::presentation::input::cli::OutputFormat;
use crate::presentation::views::commands::release::{JsonView, ReleaseDetailsData, TextView};
use crate::presentation::views::progress::{ProgressReporter, VerboseProgressListener};
use crate::presentation::views::UserOutput;
use crate::shared::clock::Clock;
Expand Down Expand Up @@ -109,12 +113,16 @@ impl ReleaseCommandController {
///
/// Returns `Ok(())` on success, or a `ReleaseSubcommandError` if any step fails.
#[allow(clippy::result_large_err)]
pub async fn execute(&mut self, environment_name: &str) -> Result<(), ReleaseSubcommandError> {
pub async fn execute(
&mut self,
environment_name: &str,
output_format: OutputFormat,
) -> Result<(), ReleaseSubcommandError> {
let env_name = self.validate_environment_name(environment_name)?;

self.release_application(&env_name).await?;
let released_env = self.release_application(&env_name).await?;

self.complete_workflow(environment_name)?;
self.complete_workflow(&released_env, output_format)?;

Ok(())
}
Expand Down Expand Up @@ -151,7 +159,7 @@ impl ReleaseCommandController {
async fn release_application(
&mut self,
env_name: &EnvironmentName,
) -> Result<(), ReleaseSubcommandError> {
) -> Result<Environment<Released>, ReleaseSubcommandError> {
self.progress
.start_step(ReleaseStep::ReleaseApplication.description())?;

Expand All @@ -162,7 +170,7 @@ impl ReleaseCommandController {
// user-facing detail messages via UserOutput's verbosity filter.
let listener = VerboseProgressListener::new(self.progress.output().clone());

let _released_env = handler
let released_env = handler
.execute(env_name, Some(&listener))
.await
.map_err(|source| ReleaseSubcommandError::ApplicationLayerError { source })?;
Expand All @@ -176,17 +184,28 @@ impl ReleaseCommandController {
self.progress
.complete_step(Some("Application released successfully"))?;

Ok(())
Ok(released_env)
}

/// Complete the workflow with success message
/// Complete the workflow with environment details output
///
/// Shows final success message to the user with workflow summary.
/// Renders the released environment details using the chosen output format
/// (text or JSON) and displays them to the user.
#[allow(clippy::result_large_err)]
fn complete_workflow(&mut self, name: &str) -> Result<(), ReleaseSubcommandError> {
self.progress.complete(&format!(
"Release command completed successfully for '{name}'"
))?;
fn complete_workflow(
&mut self,
released_env: &Environment<Released>,
output_format: OutputFormat,
) -> Result<(), ReleaseSubcommandError> {
let details = ReleaseDetailsData::from(released_env);

let output = match output_format {
OutputFormat::Text => TextView::render(&details),
OutputFormat::Json => JsonView::render(&details),
};

self.progress.result(&output)?;

Ok(())
}
}
Expand Down Expand Up @@ -228,7 +247,7 @@ mod tests {

// Test with invalid environment name (contains underscore)
let result = ReleaseCommandController::new(repository, clock, user_output.clone())
.execute("invalid_name")
.execute("invalid_name", OutputFormat::Text)
.await;

assert!(result.is_err());
Expand All @@ -247,7 +266,7 @@ mod tests {
let (user_output, repository, clock) = create_test_dependencies(&temp_dir);

let result = ReleaseCommandController::new(repository, clock, user_output.clone())
.execute("")
.execute("", OutputFormat::Text)
.await;

assert!(result.is_err());
Expand All @@ -267,7 +286,7 @@ mod tests {

// Valid environment name but environment doesn't exist
let result = ReleaseCommandController::new(repository, clock, user_output.clone())
.execute("test-env")
.execute("test-env", OutputFormat::Text)
.await;

// Should fail because environment doesn't exist
Expand Down
11 changes: 6 additions & 5 deletions src/presentation/controllers/release/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::domain::environment::repository::EnvironmentRepository;
use crate::infrastructure::persistence::repository_factory::RepositoryFactory;
use crate::presentation::controllers::constants::DEFAULT_LOCK_TIMEOUT;
use crate::presentation::controllers::release::handler::ReleaseCommandController;
use crate::presentation::input::cli::OutputFormat;
use crate::presentation::views::testing::TestUserOutput;
use crate::presentation::views::{UserOutput, VerbosityLevel};
use crate::shared::clock::Clock;
Expand Down Expand Up @@ -46,7 +47,7 @@ mod environment_name_validation {
let (user_output, repository, clock) = create_test_dependencies(&temp_dir);

let result = ReleaseCommandController::new(repository, clock, user_output)
.execute("invalid_name")
.execute("invalid_name", OutputFormat::Text)
.await;

assert!(matches!(
Expand All @@ -61,7 +62,7 @@ mod environment_name_validation {
let (user_output, repository, clock) = create_test_dependencies(&temp_dir);

let result = ReleaseCommandController::new(repository, clock, user_output)
.execute("")
.execute("", OutputFormat::Text)
.await;

assert!(matches!(
Expand All @@ -76,7 +77,7 @@ mod environment_name_validation {
let (user_output, repository, clock) = create_test_dependencies(&temp_dir);

let result = ReleaseCommandController::new(repository, clock, user_output)
.execute("-invalid")
.execute("-invalid", OutputFormat::Text)
.await;

assert!(matches!(
Expand All @@ -97,7 +98,7 @@ mod workflow_errors {

// Valid name but environment doesn't exist
let result = ReleaseCommandController::new(repository, clock, user_output)
.execute("production")
.execute("production", OutputFormat::Text)
.await;

// Should fail with ApplicationLayerError because environment doesn't exist
Expand All @@ -116,7 +117,7 @@ mod workflow_errors {
let (user_output, repository, clock) = create_test_dependencies(&temp_dir);

let result = ReleaseCommandController::new(repository, clock, user_output)
.execute("my-test-env")
.execute("my-test-env", OutputFormat::Text)
.await;

// Should fail with ApplicationLayerError because environment doesn't exist
Expand Down
3 changes: 2 additions & 1 deletion src/presentation/dispatch/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,11 @@ pub async fn route_command(
Ok(())
}
Commands::Release { environment } => {
let output_format = context.output_format();
context
.container()
.create_release_controller()
.execute(&environment)
.execute(&environment, output_format)
.await?;
Ok(())
}
Expand Down
1 change: 1 addition & 0 deletions src/presentation/views/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod configure;
pub mod create;
pub mod list;
pub mod provision;
pub mod release;
pub mod run;
pub mod shared;
pub mod show;
52 changes: 52 additions & 0 deletions src/presentation/views/commands/release/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//! Views for Release Command
//!
//! This module contains view components for rendering release command output.
//!
//! # Architecture
//!
//! This module follows the Strategy Pattern for rendering:
//! - `ReleaseDetailsData`: The data DTO passed to all views
//! - `TextView`: Renders human-readable text output
//! - `JsonView`: Renders machine-readable JSON output
//!
//! # Structure
//!
//! - `view_data/`: Data structures (DTOs) passed to views
//! - `release_details.rs`: Main DTO with environment release data
//! - `views/`: View rendering implementations
//! - `text_view.rs`: Human-readable text rendering
//! - `json_view.rs`: Machine-readable JSON rendering
//!
//! # SOLID Principles
//!
//! - **Single Responsibility**: Each view has one job - render in its format
//! - **Open/Closed**: Add new formats by creating new view files, not modifying existing ones
//! - **Strategy Pattern**: Different rendering strategies for the same data
//!
//! # Adding New Formats
//!
//! To add a new output format (e.g., XML, YAML, CSV):
//! 1. Create a new file in `views/`: `xml_view.rs`, `yaml_view.rs`, etc.
//! 2. Implement the view with `render(data: &ReleaseDetailsData) -> String`
//! 3. Export it from this module
//! 4. No need to modify existing views or the DTO

pub mod view_data {
pub mod release_details;

// Re-export main types for convenience
pub use release_details::ReleaseDetailsData;
}

pub mod views {
pub mod json_view;
pub mod text_view;

// Re-export views for convenience
pub use json_view::JsonView;
pub use text_view::TextView;
}

// Re-export at module root for convenience
pub use view_data::ReleaseDetailsData;
pub use views::{JsonView, TextView};
Loading
Loading