diff --git a/docs/refactors/plans/standardize-command-view-folder-structure.md b/docs/refactors/plans/standardize-command-view-folder-structure.md index a65288d0..ec003862 100644 --- a/docs/refactors/plans/standardize-command-view-folder-structure.md +++ b/docs/refactors/plans/standardize-command-view-folder-structure.md @@ -61,14 +61,14 @@ harder to review (logic changes + file moves mixed). The correct order is: **Total Active Proposals**: 3 **Total Postponed**: 0 **Total Discarded**: 0 -**Completed**: 0 +**Completed**: 3 **In Progress**: 0 -**Not Started**: 3 +**Not Started**: 0 ### Phase Summary -- **Phase 0 - Fix `run` module layout (High Impact, Low Effort)**: ⏳ 0/2 completed (0%) -- **Phase 1 - Fix `list` and `show` module layout (Medium Impact, Low Effort)**: ⏳ 0/1 completed (0%) +- **Phase 0 - Fix `run` module layout (High Impact, Low Effort)**: ✅ 2/2 completed (100%) +- **Phase 1 - Fix `list` and `show` module layout (Medium Impact, Low Effort)**: ✅ 1/1 completed (100%) ### Discarded Proposals @@ -134,13 +134,13 @@ For `list` and `show`, this means creating thin wrapper or re-export modules in ### Proposal #1: Move `run` View Files Into `views/` Subfolder -**Status**: ⏳ Not Started +**Status**: ✅ Completed **Impact**: 🟢🟢🟢 High **Effort**: 🔵 Low **Priority**: P0 **Depends On**: None -**Completed**: - -**Commit**: - +**Completed**: 2026-02-28 +**Commit**: `8e3f8a41` #### Problem @@ -193,13 +193,13 @@ All public exports from `run/mod.rs` remain the same — callers are unaffected. #### Implementation Checklist -- [ ] Create directory `src/presentation/cli/views/commands/run/views/` -- [ ] Move `run/json_view.rs` to `run/views/json_view.rs` -- [ ] Move `run/text_view.rs` to `run/views/text_view.rs` -- [ ] Create `run/views/mod.rs` exporting `JsonView` and `TextView` -- [ ] Update `run/mod.rs` to use `mod views` and re-export through it -- [ ] Verify all tests pass -- [ ] Run `cargo run --bin linter all` and fix issues +- [x] Create directory `src/presentation/cli/views/commands/run/views/` +- [x] Move `run/json_view.rs` to `run/views/json_view.rs` +- [x] Move `run/text_view.rs` to `run/views/text_view.rs` +- [x] Create `run/views/mod.rs` exporting `JsonView` and `TextView` +- [x] Update `run/mod.rs` to use `mod views` and re-export through it +- [x] Verify all tests pass +- [x] Run `cargo run --bin linter all` and fix issues #### Testing Strategy @@ -210,13 +210,13 @@ E2E tests must pass unchanged. ### Proposal #2: Extract `run` Inline DTO to `view_data/` -**Status**: ⏳ Not Started +**Status**: ✅ Completed **Impact**: 🟢🟢 Medium **Effort**: 🔵 Low **Priority**: P0 **Depends On**: Proposal #1 -**Completed**: - -**Commit**: - +**Completed**: 2026-02-28 +**Commit**: `ff0c6d7b` #### Problem @@ -279,14 +279,14 @@ in other commands (e.g., `ConfigureDetailsData`, `DestroyDetailsData`). #### Implementation Checklist -- [ ] Create `src/presentation/cli/views/commands/run/view_data/run_details.rs` with `RunDetailsData` -- [ ] Create `src/presentation/cli/views/commands/run/view_data/mod.rs` -- [ ] Update `run/mod.rs` to declare `pub mod view_data` -- [ ] Update `run/views/json_view.rs` to use `RunDetailsData` from `super::super::view_data` +- [x] Create `src/presentation/cli/views/commands/run/view_data/run_details.rs` with `RunDetailsData` +- [x] Create `src/presentation/cli/views/commands/run/view_data/mod.rs` +- [x] Update `run/mod.rs` to declare `pub mod view_data` +- [x] Update `run/views/json_view.rs` to use `RunDetailsData` from `super::super::view_data` (or the crate-level path) and remove the inline struct -- [ ] Update all call sites constructing the old inline struct to construct `RunDetailsData` -- [ ] Verify all tests pass -- [ ] Run `cargo run --bin linter all` and fix issues +- [x] Update all call sites constructing the old inline struct to construct `RunDetailsData` +- [x] Verify all tests pass +- [x] Run `cargo run --bin linter all` and fix issues #### Testing Strategy @@ -299,13 +299,13 @@ old struct. Observable JSON output is unaffected (field names do not change). ### Proposal #3: Add `view_data/` to `list` and `show` -**Status**: ⏳ Not Started +**Status**: ✅ Completed **Impact**: 🟢🟢 Medium **Effort**: 🔵 Low **Priority**: P1 **Depends On**: None (can be parallel with Phase 0) -**Completed**: - -**Commit**: - +**Completed**: 2026-02-28 +**Commit**: `3d63edc6` #### Problem @@ -379,17 +379,17 @@ file in the command. #### Implementation Checklist -- [ ] Create `src/presentation/cli/views/commands/list/view_data/list_details.rs` -- [ ] Create `src/presentation/cli/views/commands/list/view_data/mod.rs` -- [ ] Update `list/mod.rs` to declare `pub mod view_data` -- [ ] Update imports in `list/views/json_view.rs` and `list/views/text_view.rs` -- [ ] Create `src/presentation/cli/views/commands/show/view_data/show_details.rs` -- [ ] Create `src/presentation/cli/views/commands/show/view_data/mod.rs` -- [ ] Update `show/mod.rs` to declare `pub mod view_data` -- [ ] Update imports in `show/views/json_view.rs`, `show/views/text_view.rs`, +- [x] Create `src/presentation/cli/views/commands/list/view_data/list_details.rs` +- [x] Create `src/presentation/cli/views/commands/list/view_data/mod.rs` +- [x] Update `list/mod.rs` to declare `pub mod view_data` +- [x] Update imports in `list/views/json_view.rs` and `list/views/text_view.rs` +- [x] Create `src/presentation/cli/views/commands/show/view_data/show_details.rs` +- [x] Create `src/presentation/cli/views/commands/show/view_data/mod.rs` +- [x] Update `show/mod.rs` to declare `pub mod view_data` +- [x] Update imports in `show/views/json_view.rs`, `show/views/text_view.rs`, and all other files under `show/views/` that import application-layer types -- [ ] Verify all tests pass -- [ ] Run `cargo run --bin linter all` and fix issues +- [x] Verify all tests pass +- [x] Run `cargo run --bin linter all` and fix issues #### Testing Strategy @@ -406,18 +406,18 @@ Pure import path changes. Compilation is the primary test. No behavior changes. ### Approval Criteria -- [ ] Technical feasibility validated -- [ ] Aligns with [Development Principles](../development-principles.md) -- [ ] Implementation plan is clear and actionable -- [ ] Priorities are correct (high-impact/low-effort first) +- [x] Technical feasibility validated +- [x] Aligns with [Development Principles](../development-principles.md) +- [x] Implementation plan is clear and actionable +- [x] Priorities are correct (high-impact/low-effort first) ### Completion Criteria -- [ ] All active proposals implemented -- [ ] All tests passing -- [ ] All linters passing -- [ ] Documentation updated -- [ ] Changes merged to main branch +- [x] All active proposals implemented +- [x] All tests passing +- [x] All linters passing +- [x] Documentation updated +- [x] Changes merged to main branch ## 📚 Related Documentation @@ -442,5 +442,5 @@ The `show` command's `views/` subfolder already contains multiple sub-views --- **Created**: 2026-02-27 -**Last Updated**: 2026-02-27 -**Status**: 📋 Planning +**Last Updated**: 2026-02-28 +**Status**: ✅ Completed diff --git a/src/presentation/cli/controllers/run/handler.rs b/src/presentation/cli/controllers/run/handler.rs index 60c1ae7a..68ad11ba 100644 --- a/src/presentation/cli/controllers/run/handler.rs +++ b/src/presentation/cli/controllers/run/handler.rs @@ -14,7 +14,7 @@ use crate::domain::environment::name::EnvironmentName; use crate::domain::environment::repository::EnvironmentRepository; use crate::domain::environment::state::AnyEnvironmentState; use crate::presentation::cli::input::cli::OutputFormat; -use crate::presentation::cli::views::commands::run::{JsonView, TextView}; +use crate::presentation::cli::views::commands::run::{JsonView, RunDetailsData, TextView}; use crate::presentation::cli::views::progress::ProgressReporter; use crate::presentation::cli::views::UserOutput; use crate::shared::clock::Clock; @@ -266,17 +266,15 @@ impl RunCommandController { let grafana = grafana_config.map(|config| GrafanaInfo::from_config(config, instance_ip)); + let data = RunDetailsData::new(any_env.name().to_string(), services, grafana); + // Render using appropriate view based on output format (Strategy Pattern) let output = match output_format { - OutputFormat::Text => { - TextView::render(any_env.name().as_str(), &services, grafana.as_ref()) - } - OutputFormat::Json => { - JsonView::render(any_env.name().as_str(), &services, grafana.as_ref()) - } + OutputFormat::Text => TextView::render(&data), + OutputFormat::Json => JsonView::render(&data), }; - // Pipeline: ServiceInfo + GrafanaInfo → render → output to stdout + // Pipeline: RunDetailsData → render → output to stdout self.progress.result(&output)?; } diff --git a/src/presentation/cli/views/commands/list/mod.rs b/src/presentation/cli/views/commands/list/mod.rs index 31fdb8cc..ba1dc6db 100644 --- a/src/presentation/cli/views/commands/list/mod.rs +++ b/src/presentation/cli/views/commands/list/mod.rs @@ -14,6 +14,7 @@ //! - `text_view.rs`: Human-readable table rendering //! - `json_view.rs`: JSON output for automation workflows +pub mod view_data; pub mod views { pub mod json_view; pub mod text_view; @@ -24,4 +25,5 @@ pub mod views { } // Re-export everything at the module level for backward compatibility +pub use view_data::EnvironmentList; pub use views::{JsonView, TextView}; diff --git a/src/presentation/cli/views/commands/list/view_data/list_details.rs b/src/presentation/cli/views/commands/list/view_data/list_details.rs new file mode 100644 index 00000000..493cf628 --- /dev/null +++ b/src/presentation/cli/views/commands/list/view_data/list_details.rs @@ -0,0 +1,8 @@ +//! View data for the list command. +//! +//! Re-exports the application-layer DTOs as the canonical view input types. +//! The presentation layer references this module rather than importing directly +//! from the application layer. + +pub use crate::application::command_handlers::list::info::EnvironmentList; +pub use crate::application::command_handlers::list::info::EnvironmentSummary; diff --git a/src/presentation/cli/views/commands/list/view_data/mod.rs b/src/presentation/cli/views/commands/list/view_data/mod.rs new file mode 100644 index 00000000..098caadf --- /dev/null +++ b/src/presentation/cli/views/commands/list/view_data/mod.rs @@ -0,0 +1,3 @@ +pub mod list_details; + +pub use list_details::{EnvironmentList, EnvironmentSummary}; diff --git a/src/presentation/cli/views/commands/list/views/json_view.rs b/src/presentation/cli/views/commands/list/views/json_view.rs index 547d2e58..17429d08 100644 --- a/src/presentation/cli/views/commands/list/views/json_view.rs +++ b/src/presentation/cli/views/commands/list/views/json_view.rs @@ -9,7 +9,7 @@ //! The `JsonView` serializes environment list information to JSON using `serde_json`. //! The output includes environment summaries, failed environments, and metadata. -use crate::application::command_handlers::list::info::EnvironmentList; +use crate::presentation::cli::views::commands::list::view_data::EnvironmentList; /// View for rendering environment list as JSON /// @@ -115,7 +115,7 @@ mod tests { use serde_json::Value; use super::*; - use crate::application::command_handlers::list::info::EnvironmentSummary; + use crate::presentation::cli::views::commands::list::view_data::EnvironmentSummary; #[test] fn it_should_render_empty_environment_list_as_json() { diff --git a/src/presentation/cli/views/commands/list/views/text_view.rs b/src/presentation/cli/views/commands/list/views/text_view.rs index 7be1b688..761d6ccd 100644 --- a/src/presentation/cli/views/commands/list/views/text_view.rs +++ b/src/presentation/cli/views/commands/list/views/text_view.rs @@ -4,7 +4,7 @@ //! It follows the Strategy Pattern, providing one specific rendering strategy //! (human-readable text table) for environment lists. -use crate::application::command_handlers::list::info::EnvironmentList; +use crate::presentation::cli::views::commands::list::view_data::EnvironmentList; /// Text view for rendering environment list /// @@ -159,7 +159,7 @@ impl TextView { #[cfg(test)] mod tests { use super::*; - use crate::application::command_handlers::list::info::EnvironmentSummary; + use crate::presentation::cli::views::commands::list::view_data::EnvironmentSummary; #[test] fn it_should_render_empty_workspace() { diff --git a/src/presentation/cli/views/commands/run/mod.rs b/src/presentation/cli/views/commands/run/mod.rs index a79424c0..38130b6f 100644 --- a/src/presentation/cli/views/commands/run/mod.rs +++ b/src/presentation/cli/views/commands/run/mod.rs @@ -4,8 +4,9 @@ //! Following the Strategy Pattern, each view (`TextView`, `JsonView`) implements //! a different output format for the same underlying data (`ServiceInfo` and `GrafanaInfo` DTOs). -mod json_view; -mod text_view; +pub mod view_data; +mod views; -pub use json_view::JsonView; -pub use text_view::TextView; +pub use view_data::RunDetailsData; +pub use views::JsonView; +pub use views::TextView; diff --git a/src/presentation/cli/views/commands/run/view_data/mod.rs b/src/presentation/cli/views/commands/run/view_data/mod.rs new file mode 100644 index 00000000..b13a88b2 --- /dev/null +++ b/src/presentation/cli/views/commands/run/view_data/mod.rs @@ -0,0 +1,8 @@ +//! View data for the Run Command +//! +//! This module contains data transfer objects (DTOs) used to pass data +//! to run command view renderers. + +pub mod run_details; + +pub use run_details::RunDetailsData; diff --git a/src/presentation/cli/views/commands/run/view_data/run_details.rs b/src/presentation/cli/views/commands/run/view_data/run_details.rs new file mode 100644 index 00000000..e66f2c48 --- /dev/null +++ b/src/presentation/cli/views/commands/run/view_data/run_details.rs @@ -0,0 +1,69 @@ +//! Run Details Data Transfer Object +//! +//! This module contains the presentation DTO for the run command output. +//! It serves as the data structure passed to view renderers (`TextView`, `JsonView`, etc.). +//! +//! # Architecture +//! +//! This follows the Strategy Pattern where: +//! - This DTO is the data passed to all rendering strategies +//! - Different views (`TextView`, `JsonView`) consume this data +//! - Adding new formats doesn't modify this DTO or existing views +//! +//! # SOLID Principles +//! +//! - **Single Responsibility**: This file only defines the data structure +//! - **Open/Closed**: New formats extend by adding views, not modifying this +//! - **Separation of Concerns**: Data definition separate from rendering logic + +use serde::Serialize; + +use crate::application::command_handlers::show::info::{GrafanaInfo, ServiceInfo}; + +/// Run details data for rendering +/// +/// This struct holds all the data needed to render run command output +/// for display to the user. It is consumed by view renderers +/// (`TextView`, `JsonView`) which format it according to their specific output format. +/// +/// The `state` field is always `"Running"` — this command only executes +/// when services are being started successfully. +#[derive(Debug, Serialize)] +pub struct RunDetailsData { + /// Name of the environment + pub environment_name: String, + + /// Current state — always `"Running"` for this command + pub state: String, + + /// Tracker and API service endpoint information + pub services: ServiceInfo, + + /// Optional Grafana dashboard information + pub grafana: Option, +} + +impl RunDetailsData { + /// Create a new `RunDetailsData` + /// + /// The `state` field is always set to `"Running"`. + /// + /// # Arguments + /// + /// * `environment_name` - Name of the environment + /// * `services` - Tracker and API service endpoint information + /// * `grafana` - Optional Grafana dashboard information + #[must_use] + pub fn new( + environment_name: String, + services: ServiceInfo, + grafana: Option, + ) -> Self { + Self { + environment_name, + state: "Running".to_string(), + services, + grafana, + } + } +} diff --git a/src/presentation/cli/views/commands/run/json_view.rs b/src/presentation/cli/views/commands/run/views/json_view.rs similarity index 73% rename from src/presentation/cli/views/commands/run/json_view.rs rename to src/presentation/cli/views/commands/run/views/json_view.rs index a370003a..0d8cafa1 100644 --- a/src/presentation/cli/views/commands/run/json_view.rs +++ b/src/presentation/cli/views/commands/run/views/json_view.rs @@ -2,28 +2,15 @@ //! //! This module provides JSON-based rendering for run command output. //! It follows the Strategy Pattern, providing a machine-readable output format -//! for the same underlying data (`ServiceInfo` and `GrafanaInfo` DTOs). +//! for the same underlying data (`RunDetailsData` DTO). //! //! # Design //! -//! The `JsonView` serializes service information to JSON using `serde_json`. +//! The `JsonView` serializes the `RunDetailsData` DTO to JSON using `serde_json`. //! The output includes the environment name, state (always "Running"), and //! service information from the existing DTOs. -use serde::Serialize; - -use crate::application::command_handlers::show::info::{GrafanaInfo, ServiceInfo}; - -/// DTO for JSON output of run command -/// -/// This structure wraps the service information for JSON serialization. -#[derive(Debug, Serialize)] -struct RunCommandOutput<'a> { - environment_name: &'a str, - state: &'a str, - services: &'a ServiceInfo, - grafana: Option<&'a GrafanaInfo>, -} +use crate::presentation::cli::views::commands::run::view_data::RunDetailsData; /// View for rendering run command output as JSON /// @@ -35,7 +22,7 @@ struct RunCommandOutput<'a> { /// /// ```rust /// use torrust_tracker_deployer_lib::application::command_handlers::show::info::ServiceInfo; -/// use torrust_tracker_deployer_lib::presentation::cli::views::commands::run::JsonView; +/// use torrust_tracker_deployer_lib::presentation::cli::views::commands::run::{JsonView, RunDetailsData}; /// /// let services = ServiceInfo::new( /// vec!["udp://10.0.0.1:6969/announce".to_string()], @@ -51,7 +38,8 @@ struct RunCommandOutput<'a> { /// vec![], /// ); /// -/// let output = JsonView::render("my-env", &services, None); +/// let data = RunDetailsData::new("my-env".to_string(), services, None); +/// let output = JsonView::render(&data); /// // Verify it's valid JSON /// let parsed: serde_json::Value = serde_json::from_str(&output).unwrap(); /// assert_eq!(parsed["environment_name"], "my-env"); @@ -62,15 +50,12 @@ pub struct JsonView; impl JsonView { /// Render run command output as JSON /// - /// Serializes the service information to pretty-printed JSON format. - /// The state is always "Running" since this command only executes - /// when services are being started. + /// Serializes the `RunDetailsData` DTO to pretty-printed JSON format. /// /// # Arguments /// - /// * `env_name` - Name of the environment - /// * `services` - Service information containing tracker endpoints - /// * `grafana` - Optional Grafana service information + /// * `data` - Run details DTO containing environment name, state, + /// service endpoints, and optional Grafana information /// /// # Returns /// @@ -84,7 +69,7 @@ impl JsonView { /// use torrust_tracker_deployer_lib::application::command_handlers::show::info::{ /// ServiceInfo, GrafanaInfo, /// }; - /// use torrust_tracker_deployer_lib::presentation::cli::views::commands::run::JsonView; + /// use torrust_tracker_deployer_lib::presentation::cli::views::commands::run::{JsonView, RunDetailsData}; /// use url::Url; /// /// let services = ServiceInfo::new( @@ -106,7 +91,8 @@ impl JsonView { /// false, /// ); /// - /// let json = JsonView::render("my-env", &services, Some(&grafana)); + /// let data = RunDetailsData::new("my-env".to_string(), services, Some(grafana)); + /// let json = JsonView::render(&data); /// // Verify it's valid JSON and has expected fields /// let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); /// assert_eq!(parsed["environment_name"], "my-env"); @@ -114,15 +100,8 @@ impl JsonView { /// assert!(parsed["grafana"].is_object()); /// ``` #[must_use] - pub fn render(env_name: &str, services: &ServiceInfo, grafana: Option<&GrafanaInfo>) -> String { - let output = RunCommandOutput { - environment_name: env_name, - state: "Running", - services, - grafana, - }; - - serde_json::to_string_pretty(&output).unwrap_or_else(|e| { + pub fn render(data: &RunDetailsData) -> String { + serde_json::to_string_pretty(data).unwrap_or_else(|e| { serde_json::to_string_pretty(&serde_json::json!({ "error": "Failed to serialize run details", "message": e.to_string(), @@ -141,10 +120,12 @@ impl JsonView { mod tests { use url::Url; + use crate::application::command_handlers::show::info::{GrafanaInfo, ServiceInfo}; + use super::*; - fn sample_basic_services() -> ServiceInfo { - ServiceInfo::new( + fn sample_data() -> RunDetailsData { + let services = ServiceInfo::new( vec!["udp://udp.tracker.local:6969/announce".to_string()], vec![], vec!["http://10.140.190.133:7070/announce".to_string()], @@ -156,15 +137,15 @@ mod tests { false, false, vec![], - ) + ); + RunDetailsData::new("test-env".to_string(), services, None) } #[test] fn it_should_render_basic_json_output() { - let services = sample_basic_services(); - let output = JsonView::render("test-env", &services, None); + let data = sample_data(); + let output = JsonView::render(&data); - // Verify JSON structure assert!( output.contains(r#""environment_name":"test-env""#) || output.contains(r#""environment_name": "test-env""#) @@ -178,12 +159,24 @@ mod tests { #[test] fn it_should_include_grafana_when_provided() { - let services = sample_basic_services(); + let services = ServiceInfo::new( + vec!["udp://udp.tracker.local:6969/announce".to_string()], + vec![], + vec!["http://10.140.190.133:7070/announce".to_string()], + vec![], + "http://10.140.190.133:1212/api".to_string(), + false, + false, + "http://10.140.190.133:1313/health_check".to_string(), + false, + false, + vec![], + ); let grafana = GrafanaInfo::new(Url::parse("http://10.140.190.133:3000").unwrap(), false); + let data = RunDetailsData::new("test-env".to_string(), services, Some(grafana)); - let output = JsonView::render("test-env", &services, Some(&grafana)); + let output = JsonView::render(&data); - // Verify grafana section exists assert!(output.contains(r#""grafana":"#)); assert!( output.contains(r#""url":"http://10.140.190.133:3000/""#) @@ -196,14 +189,12 @@ mod tests { #[test] fn it_should_produce_valid_json() { - let services = sample_basic_services(); - let output = JsonView::render("test-env", &services, None); + let data = sample_data(); + let output = JsonView::render(&data); - // Verify it's valid JSON by parsing it let parsed: serde_json::Value = serde_json::from_str(&output).expect("Output should be valid JSON"); - // Verify key fields assert_eq!(parsed["environment_name"], "test-env"); assert_eq!(parsed["state"], "Running"); assert!(parsed["services"].is_object()); @@ -212,13 +203,12 @@ mod tests { #[test] fn it_should_include_all_service_fields() { - let services = sample_basic_services(); - let output = JsonView::render("test-env", &services, None); + let data = sample_data(); + let output = JsonView::render(&data); let parsed: serde_json::Value = serde_json::from_str(&output).unwrap(); let services_obj = &parsed["services"]; - // Verify all service fields are present assert!(services_obj["udp_trackers"].is_array()); assert!(services_obj["https_http_trackers"].is_array()); assert!(services_obj["direct_http_trackers"].is_array()); diff --git a/src/presentation/cli/views/commands/run/views/mod.rs b/src/presentation/cli/views/commands/run/views/mod.rs new file mode 100644 index 00000000..52148a09 --- /dev/null +++ b/src/presentation/cli/views/commands/run/views/mod.rs @@ -0,0 +1,11 @@ +//! View implementations for the Run Command +//! +//! This module provides different rendering strategies for run command output. +//! Following the Strategy Pattern, each view (`TextView`, `JsonView`) implements +//! a different output format for the same underlying data. + +mod json_view; +mod text_view; + +pub use json_view::JsonView; +pub use text_view::TextView; diff --git a/src/presentation/cli/views/commands/run/text_view.rs b/src/presentation/cli/views/commands/run/views/text_view.rs similarity index 63% rename from src/presentation/cli/views/commands/run/text_view.rs rename to src/presentation/cli/views/commands/run/views/text_view.rs index 1845711a..29429fb8 100644 --- a/src/presentation/cli/views/commands/run/text_view.rs +++ b/src/presentation/cli/views/commands/run/views/text_view.rs @@ -3,7 +3,7 @@ //! This module provides human-readable text rendering for the run command. //! It displays service URLs, DNS hints, and helpful tips after services are started. -use crate::application::command_handlers::show::info::{GrafanaInfo, ServiceInfo}; +use crate::presentation::cli::views::commands::run::view_data::RunDetailsData; use crate::presentation::cli::views::commands::shared::service_urls::{ CompactServiceUrlsView, DnsHintView, }; @@ -18,7 +18,7 @@ use crate::presentation::cli::views::commands::shared::service_urls::{ /// /// ```rust /// use torrust_tracker_deployer_lib::application::command_handlers::show::info::ServiceInfo; -/// use torrust_tracker_deployer_lib::presentation::cli::views::commands::run::TextView; +/// use torrust_tracker_deployer_lib::presentation::cli::views::commands::run::{TextView, RunDetailsData}; /// /// let services = ServiceInfo::new( /// vec!["udp://10.0.0.1:6969/announce".to_string()], @@ -34,7 +34,8 @@ use crate::presentation::cli::views::commands::shared::service_urls::{ /// vec![], /// ); /// -/// let output = TextView::render("my-env", &services, None); +/// let data = RunDetailsData::new("my-env".to_string(), services, None); +/// let output = TextView::render(&data); /// assert!(output.contains("Services are now accessible:")); /// assert!(output.contains("Tip:")); /// ``` @@ -45,32 +46,33 @@ impl TextView { /// /// # Arguments /// - /// * `env_name` - Name of the environment - /// * `services` - Service information containing tracker endpoints - /// * `grafana` - Optional Grafana service information + /// * `data` - Run details DTO containing environment name, service endpoints, + /// and optional Grafana information /// /// # Returns /// /// A formatted string with service URLs, DNS hints (if applicable), /// and a tip about using the show command for more details. #[must_use] - pub fn render(env_name: &str, services: &ServiceInfo, grafana: Option<&GrafanaInfo>) -> String { + pub fn render(data: &RunDetailsData) -> String { let mut output = Vec::new(); // Render service URLs (only public services) - let service_urls_output = CompactServiceUrlsView::render(services, grafana); + let service_urls_output = + CompactServiceUrlsView::render(&data.services, data.grafana.as_ref()); if !service_urls_output.is_empty() { output.push(format!("\n{service_urls_output}")); } // Show DNS hint if HTTPS services are configured - if let Some(dns_hint) = DnsHintView::render(services) { + if let Some(dns_hint) = DnsHintView::render(&data.services) { output.push(format!("\n{dns_hint}")); } // Show tip about show command output.push(format!( - "\nTip: Run 'torrust-tracker-deployer show {env_name}' for full details\n" + "\nTip: Run 'torrust-tracker-deployer show {}' for full details\n", + data.environment_name )); output.join("") @@ -80,9 +82,10 @@ impl TextView { #[cfg(test)] mod tests { use super::*; + use crate::application::command_handlers::show::info::ServiceInfo; - fn sample_basic_services() -> ServiceInfo { - ServiceInfo::new( + fn sample_data() -> RunDetailsData { + let services = ServiceInfo::new( vec!["udp://udp.tracker.local:6969/announce".to_string()], vec![], vec!["http://10.140.190.133:7070/announce".to_string()], @@ -94,35 +97,45 @@ mod tests { false, false, vec![], - ) + ); + RunDetailsData::new("test-env".to_string(), services, None) } #[test] fn it_should_render_basic_output() { - let services = sample_basic_services(); - let output = TextView::render("test-env", &services, None); + let data = sample_data(); + let output = TextView::render(&data); - // Should contain service URLs assert!(output.contains("Services are now accessible:")); assert!(output.contains("udp://udp.tracker.local:6969/announce")); assert!(output.contains("http://10.140.190.133:7070/announce")); assert!(output.contains("http://10.140.190.133:1212/api")); - - // Should contain tip assert!(output.contains("Tip: Run 'torrust-tracker-deployer show test-env'")); - - // Should NOT contain DNS hint (no HTTPS) assert!(!output.contains("DNS")); } #[test] fn it_should_include_grafana_when_provided() { + use crate::application::command_handlers::show::info::GrafanaInfo; use url::Url; - let services = sample_basic_services(); + let services = ServiceInfo::new( + vec!["udp://udp.tracker.local:6969/announce".to_string()], + vec![], + vec!["http://10.140.190.133:7070/announce".to_string()], + vec![], + "http://10.140.190.133:1212/api".to_string(), + false, + false, + "http://10.140.190.133:1313/health_check".to_string(), + false, + false, + vec![], + ); let grafana = GrafanaInfo::new(Url::parse("http://10.140.190.133:3000").unwrap(), false); + let data = RunDetailsData::new("test-env".to_string(), services, Some(grafana)); - let output = TextView::render("test-env", &services, Some(&grafana)); + let output = TextView::render(&data); assert!(output.contains("Grafana:")); assert!(output.contains("http://10.140.190.133:3000")); @@ -148,17 +161,31 @@ mod tests { TlsDomainInfo::new("api.tracker.local".to_string(), 1212), ], ); + let data = RunDetailsData::new("test-env".to_string(), services, None); - let output = TextView::render("test-env", &services, None); + let output = TextView::render(&data); - // Should contain DNS hint assert!(output.contains("Note: HTTPS services require DNS configuration")); } #[test] fn it_should_always_include_tip() { - let services = sample_basic_services(); - let output = TextView::render("my-environment", &services, None); + let services = ServiceInfo::new( + vec!["udp://udp.tracker.local:6969/announce".to_string()], + vec![], + vec!["http://10.140.190.133:7070/announce".to_string()], + vec![], + "http://10.140.190.133:1212/api".to_string(), + false, + false, + "http://10.140.190.133:1313/health_check".to_string(), + false, + false, + vec![], + ); + let data = RunDetailsData::new("my-environment".to_string(), services, None); + + let output = TextView::render(&data); assert!(output.contains("Tip: Run 'torrust-tracker-deployer show my-environment'")); } diff --git a/src/presentation/cli/views/commands/show/mod.rs b/src/presentation/cli/views/commands/show/mod.rs index 1e00f42d..90d7048f 100644 --- a/src/presentation/cli/views/commands/show/mod.rs +++ b/src/presentation/cli/views/commands/show/mod.rs @@ -15,7 +15,9 @@ //! - `json_view.rs`: Main `JsonView` for JSON serialization //! - Helper views: basic, infrastructure, `tracker_services`, prometheus, grafana, `https_hint`, `next_step` +pub mod view_data; pub mod views; // Re-export main types for convenience +pub use view_data::EnvironmentInfo; pub use views::{JsonView, TextView}; diff --git a/src/presentation/cli/views/commands/show/view_data/mod.rs b/src/presentation/cli/views/commands/show/view_data/mod.rs new file mode 100644 index 00000000..53ca21b5 --- /dev/null +++ b/src/presentation/cli/views/commands/show/view_data/mod.rs @@ -0,0 +1,6 @@ +pub mod show_details; + +pub use show_details::{ + EnvironmentInfo, GrafanaInfo, InfrastructureInfo, LocalhostServiceInfo, PrometheusInfo, + ServiceInfo, TlsDomainInfo, +}; diff --git a/src/presentation/cli/views/commands/show/view_data/show_details.rs b/src/presentation/cli/views/commands/show/view_data/show_details.rs new file mode 100644 index 00000000..a30be7bf --- /dev/null +++ b/src/presentation/cli/views/commands/show/view_data/show_details.rs @@ -0,0 +1,13 @@ +//! View data for the show command. +//! +//! Re-exports the application-layer DTOs as the canonical view input types. +//! The presentation layer references this module rather than importing directly +//! from the application layer. + +pub use crate::application::command_handlers::show::info::EnvironmentInfo; +pub use crate::application::command_handlers::show::info::GrafanaInfo; +pub use crate::application::command_handlers::show::info::InfrastructureInfo; +pub use crate::application::command_handlers::show::info::LocalhostServiceInfo; +pub use crate::application::command_handlers::show::info::PrometheusInfo; +pub use crate::application::command_handlers::show::info::ServiceInfo; +pub use crate::application::command_handlers::show::info::TlsDomainInfo; diff --git a/src/presentation/cli/views/commands/show/views/grafana.rs b/src/presentation/cli/views/commands/show/views/grafana.rs index b7e8daf3..7bf667ac 100644 --- a/src/presentation/cli/views/commands/show/views/grafana.rs +++ b/src/presentation/cli/views/commands/show/views/grafana.rs @@ -2,7 +2,7 @@ //! //! This module provides a view for rendering Grafana visualization service information. -use crate::application::command_handlers::show::info::GrafanaInfo; +use crate::presentation::cli::views::commands::show::view_data::GrafanaInfo; /// View for rendering Grafana service information /// diff --git a/src/presentation/cli/views/commands/show/views/https_hint.rs b/src/presentation/cli/views/commands/show/views/https_hint.rs index 2b003af4..f7016289 100644 --- a/src/presentation/cli/views/commands/show/views/https_hint.rs +++ b/src/presentation/cli/views/commands/show/views/https_hint.rs @@ -5,7 +5,7 @@ use std::net::IpAddr; -use crate::application::command_handlers::show::info::ServiceInfo; +use crate::presentation::cli::views::commands::show::view_data::ServiceInfo; /// View for rendering HTTPS configuration hints /// @@ -70,7 +70,7 @@ mod tests { use std::net::Ipv4Addr; use super::*; - use crate::application::command_handlers::show::info::TlsDomainInfo; + use crate::presentation::cli::views::commands::show::view_data::TlsDomainInfo; fn services_without_tls() -> ServiceInfo { ServiceInfo::new( diff --git a/src/presentation/cli/views/commands/show/views/infrastructure.rs b/src/presentation/cli/views/commands/show/views/infrastructure.rs index 2c2898f1..35b8b0ef 100644 --- a/src/presentation/cli/views/commands/show/views/infrastructure.rs +++ b/src/presentation/cli/views/commands/show/views/infrastructure.rs @@ -3,7 +3,7 @@ //! This module provides a view for rendering infrastructure details //! including IP address, SSH credentials, and connection commands. -use crate::application::command_handlers::show::info::InfrastructureInfo; +use crate::presentation::cli::views::commands::show::view_data::InfrastructureInfo; /// View for rendering infrastructure information /// diff --git a/src/presentation/cli/views/commands/show/views/json_view.rs b/src/presentation/cli/views/commands/show/views/json_view.rs index d90e6a88..14b4e36d 100644 --- a/src/presentation/cli/views/commands/show/views/json_view.rs +++ b/src/presentation/cli/views/commands/show/views/json_view.rs @@ -10,7 +10,7 @@ //! No transformation is needed since the DTO structure is already designed for display //! purposes and contains all necessary information in a well-structured format. -use crate::application::command_handlers::show::info::EnvironmentInfo; +use crate::presentation::cli::views::commands::show::view_data::EnvironmentInfo; /// View for rendering environment information as JSON /// @@ -112,7 +112,7 @@ mod tests { use chrono::{TimeZone, Utc}; use super::*; - use crate::application::command_handlers::show::info::InfrastructureInfo; + use crate::presentation::cli::views::commands::show::view_data::InfrastructureInfo; #[test] fn it_should_render_created_state_as_json() { diff --git a/src/presentation/cli/views/commands/show/views/prometheus.rs b/src/presentation/cli/views/commands/show/views/prometheus.rs index b82de81b..aba14b21 100644 --- a/src/presentation/cli/views/commands/show/views/prometheus.rs +++ b/src/presentation/cli/views/commands/show/views/prometheus.rs @@ -2,7 +2,7 @@ //! //! This module provides a view for rendering Prometheus metrics service information. -use crate::application::command_handlers::show::info::PrometheusInfo; +use crate::presentation::cli::views::commands::show::view_data::PrometheusInfo; /// View for rendering Prometheus service information /// diff --git a/src/presentation/cli/views/commands/show/views/text_view.rs b/src/presentation/cli/views/commands/show/views/text_view.rs index 99d20c90..be46c7b0 100644 --- a/src/presentation/cli/views/commands/show/views/text_view.rs +++ b/src/presentation/cli/views/commands/show/views/text_view.rs @@ -23,7 +23,7 @@ use super::next_step::NextStepGuidanceView; use super::prometheus::PrometheusView; use super::tracker_services::TrackerServicesView; -use crate::application::command_handlers::show::info::EnvironmentInfo; +use crate::presentation::cli::views::commands::show::view_data::EnvironmentInfo; /// View for rendering environment information /// @@ -158,7 +158,7 @@ mod tests { use chrono::{TimeZone, Utc}; use super::*; - use crate::application::command_handlers::show::info::{ + use crate::presentation::cli::views::commands::show::view_data::{ InfrastructureInfo, ServiceInfo, TlsDomainInfo, }; diff --git a/src/presentation/cli/views/commands/show/views/tracker_services.rs b/src/presentation/cli/views/commands/show/views/tracker_services.rs index fef270d0..52043121 100644 --- a/src/presentation/cli/views/commands/show/views/tracker_services.rs +++ b/src/presentation/cli/views/commands/show/views/tracker_services.rs @@ -3,7 +3,7 @@ //! This module provides a view for rendering tracker service endpoints //! including UDP trackers, HTTP trackers (HTTPS and direct), API, and health check. -use crate::application::command_handlers::show::info::ServiceInfo; +use crate::presentation::cli::views::commands::show::view_data::ServiceInfo; /// View for rendering tracker service information /// @@ -112,7 +112,9 @@ impl TrackerServicesView { #[cfg(test)] mod tests { use super::*; - use crate::application::command_handlers::show::info::{LocalhostServiceInfo, TlsDomainInfo}; + use crate::presentation::cli::views::commands::show::view_data::{ + LocalhostServiceInfo, TlsDomainInfo, + }; fn sample_http_only_services() -> ServiceInfo { ServiceInfo::new(