-
Notifications
You must be signed in to change notification settings - Fork 2
feat: add JSON output to register command (12.13) #396
#397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| //! Views for Register Command | ||
| //! | ||
| //! This module contains view components for rendering register command output. | ||
| //! | ||
| //! # Architecture | ||
| //! | ||
| //! This module follows the Strategy Pattern for rendering: | ||
| //! - `RegisterDetailsData`: 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 | ||
| //! - `register_details.rs`: Main DTO with register result 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: &RegisterDetailsData) -> String` | ||
| //! 3. Export it from this module | ||
| //! 4. No need to modify existing views or the DTO | ||
|
|
||
| pub mod view_data { | ||
| pub mod register_details; | ||
|
|
||
| // Re-export main types for convenience | ||
| pub use register_details::RegisterDetailsData; | ||
| } | ||
|
|
||
| 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::RegisterDetailsData; | ||
| pub use views::{JsonView, TextView}; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| //! Register Details Data Transfer Object | ||
| //! | ||
| //! This module contains the presentation DTO for register command details. | ||
| //! 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::domain::environment::state::Provisioned; | ||
| use crate::domain::environment::Environment; | ||
|
|
||
| /// Register details data for rendering | ||
| /// | ||
| /// This struct holds all the data needed to render register command | ||
| /// information for display to the user. It is consumed by view renderers | ||
| /// (`TextView`, `JsonView`) which format it according to their specific output format. | ||
| /// | ||
| /// # Design | ||
| /// | ||
| /// This is a presentation layer DTO (Data Transfer Object) that: | ||
| /// - Decouples application types from view formatting | ||
| /// - Provides a stable interface for multiple view strategies | ||
| /// - Contains all fields needed for any output format | ||
| /// | ||
| /// # Named Constructor | ||
| /// | ||
| /// `RegisterDetailsData` is built from an `Environment<Provisioned>` since | ||
| /// the register command handler returns the provisioned environment on success. | ||
| /// `registered: true` is always set because this DTO is only constructed on | ||
| /// the success path. | ||
| #[derive(Debug, Clone, PartialEq, Serialize)] | ||
| pub struct RegisterDetailsData { | ||
| /// Name of the environment that was registered | ||
| pub environment_name: String, | ||
| /// IP address of the registered instance (empty string if unknown) | ||
| pub instance_ip: String, | ||
| /// SSH port of the registered instance | ||
| pub ssh_port: u16, | ||
| /// Always `true` when the command exits successfully | ||
| pub registered: bool, | ||
| } | ||
|
|
||
| impl RegisterDetailsData { | ||
| /// Construct a `RegisterDetailsData` from a provisioned environment | ||
| /// | ||
| /// This named constructor always sets `registered: true` because it is only | ||
| /// called on the success path — register failures result in an error return, | ||
| /// never a `RegisterDetailsData`. | ||
| /// | ||
| /// # Arguments | ||
| /// | ||
| /// * `env` - The provisioned environment returned by the register command handler | ||
| /// | ||
| /// # Examples | ||
| /// | ||
| /// ```rust,ignore | ||
| /// use torrust_tracker_deployer_lib::presentation::cli::views::commands::register::RegisterDetailsData; | ||
| /// | ||
| /// // Built from a provisioned environment in the success path | ||
| /// let data = RegisterDetailsData::from_environment(&provisioned_env); | ||
| /// | ||
| /// assert!(data.registered); | ||
| /// assert_eq!(data.ssh_port, 22); | ||
| /// ``` | ||
| #[must_use] | ||
| pub fn from_environment(env: &Environment<Provisioned>) -> Self { | ||
| Self { | ||
| environment_name: env.name().to_string(), | ||
| instance_ip: env | ||
| .instance_ip() | ||
| .map_or_else(String::new, |ip| ip.to_string()), | ||
| ssh_port: env.ssh_port(), | ||
| registered: true, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn it_should_always_set_registered_to_true() { | ||
| // The constructor only produces RegisterDetailsData on the success path. | ||
| // We verify the field directly since we cannot construct Environment<Provisioned> | ||
| // without full infrastructure in a unit test. | ||
| let data = RegisterDetailsData { | ||
|
Comment on lines
+96
to
+99
|
||
| environment_name: "test-env".to_string(), | ||
| instance_ip: "192.168.1.1".to_string(), | ||
| ssh_port: 22, | ||
| registered: true, | ||
| }; | ||
|
|
||
| assert!( | ||
| data.registered, | ||
| "registered should always be true on success path" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn it_should_store_all_fields() { | ||
| // Arrange | ||
| let data = RegisterDetailsData { | ||
| environment_name: "my-env".to_string(), | ||
| instance_ip: "10.0.0.1".to_string(), | ||
| ssh_port: 2222, | ||
| registered: true, | ||
| }; | ||
|
|
||
| // Assert | ||
| assert_eq!(data.environment_name, "my-env"); | ||
| assert_eq!(data.instance_ip, "10.0.0.1"); | ||
| assert_eq!(data.ssh_port, 2222); | ||
| assert!(data.registered); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instance_ip()is optional, but here it’s converted to an empty string when missing. For theregistersuccess path the IP should be present (provided by the user and set during the Created→Provisioned transition), and emitting""can silently violate the expected JSON contract. Consider treatingNoneas an internal invariant violation (e.g.,expectwith a clear message) or representing absence explicitly in the DTO (e.g.,Option<IpAddr>/Option<String>) and reflecting that in the JSON view.