Skip to main content

torrust_tracker_deployer_lib/application/
errors.rs

1//! Application-layer wrapper types for domain error types
2//!
3//! These wrapper types shield the SDK's public API from internal domain error
4//! types. They mirror the domain types structurally but live in the application
5//! layer, so SDK consumers never need to import from `crate::domain`.
6//!
7//! # Design Rationale
8//!
9//! Domain errors (`RepositoryError`, `StateTypeError`, `ReleaseStep`) are
10//! implementation details of the persistence and state-machine layers.
11//! Exposing them directly in the SDK's public error surface would:
12//! 1. Require SDK consumers to add `#[allow(unused_imports)]` for domain modules
13//! 2. Make internal domain changes breaking changes for SDK consumers
14//! 3. Violate the DDD Dependency Rule (presentation ← application, not ← domain)
15//!
16//! The `From` conversions on each `*CommandHandlerError` use these wrappers so
17//! that application handler code using `?` on domain-returning calls continues
18//! to work unchanged.
19
20use std::fmt;
21
22use thiserror::Error;
23
24/// Application-layer wrapper for domain `RepositoryError`.
25///
26/// Mirrors the three variants of the domain type using plain types (no domain
27/// imports required by SDK consumers).
28///
29/// # Examples
30///
31/// ```rust
32/// use torrust_tracker_deployer_lib::application::errors::PersistenceError;
33///
34/// let err = PersistenceError::NotFound;
35/// assert_eq!(err.to_string(), "Environment not found");
36///
37/// let internal = PersistenceError::Internal(
38///     anyhow::anyhow!("disk full")
39/// );
40/// assert!(internal.to_string().contains("Internal error"));
41/// ```
42#[derive(Debug, Error)]
43pub enum PersistenceError {
44    /// Environment not found in storage
45    #[error("Environment not found")]
46    NotFound,
47
48    /// Conflict with concurrent operation
49    #[error("Conflict: another process is accessing this environment")]
50    Conflict,
51
52    /// Internal implementation-specific error
53    #[error("Internal error: {0}")]
54    Internal(#[source] anyhow::Error),
55}
56
57impl From<crate::domain::environment::repository::RepositoryError> for PersistenceError {
58    fn from(e: crate::domain::environment::repository::RepositoryError) -> Self {
59        use crate::domain::environment::repository::RepositoryError;
60        match e {
61            RepositoryError::NotFound => Self::NotFound,
62            RepositoryError::Conflict => Self::Conflict,
63            RepositoryError::Internal(inner) => Self::Internal(inner),
64        }
65    }
66}
67
68/// Application-layer wrapper for domain `StateTypeError`.
69///
70/// Uses owned `String` fields instead of the domain's `&'static str` / `String`
71/// mix, so SDK consumers only deal with plain strings.
72///
73/// # Examples
74///
75/// ```rust
76/// use torrust_tracker_deployer_lib::application::errors::InvalidStateError;
77///
78/// let err = InvalidStateError {
79///     expected: "provisioned".to_string(),
80///     actual: "created".to_string(),
81/// };
82/// assert!(err.to_string().contains("provisioned"));
83/// ```
84#[derive(Debug, Error)]
85#[error("Expected state '{expected}', but found '{actual}'")]
86pub struct InvalidStateError {
87    /// The state that was expected
88    pub expected: String,
89    /// The actual state at the time of the error
90    pub actual: String,
91}
92
93impl From<crate::domain::environment::state::StateTypeError> for InvalidStateError {
94    fn from(e: crate::domain::environment::state::StateTypeError) -> Self {
95        use crate::domain::environment::state::StateTypeError;
96        match e {
97            StateTypeError::UnexpectedState { expected, actual } => Self {
98                expected: expected.to_string(),
99                actual,
100            },
101        }
102    }
103}
104
105/// Application-layer representation of a release workflow step.
106///
107/// Mirrors [`crate::domain::environment::state::ReleaseStep`] for error
108/// reporting in `ReleaseCommandHandlerError` variants that need to surface
109/// the step that failed. SDK consumers can display or match on these variants
110/// without importing the domain module.
111///
112/// # Examples
113///
114/// ```rust
115/// use torrust_tracker_deployer_lib::application::errors::ReleaseWorkflowStep;
116///
117/// let step = ReleaseWorkflowStep::InitTrackerDatabase;
118/// assert_eq!(step.to_string(), "Initialize Tracker Database");
119/// ```
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum ReleaseWorkflowStep {
122    /// Creating tracker storage directories on remote host
123    CreateTrackerStorage,
124    /// Initializing tracker `SQLite` database file
125    InitTrackerDatabase,
126    /// Rendering Tracker configuration templates to the build directory
127    RenderTrackerTemplates,
128    /// Deploying tracker configuration to the remote host via Ansible
129    DeployTrackerConfigToRemote,
130    /// Creating Prometheus storage directories on remote host
131    CreatePrometheusStorage,
132    /// Rendering Prometheus configuration templates to the build directory
133    RenderPrometheusTemplates,
134    /// Deploying Prometheus configuration to the remote host via Ansible
135    DeployPrometheusConfigToRemote,
136    /// Creating Grafana storage directories on remote host
137    CreateGrafanaStorage,
138    /// Rendering Grafana provisioning templates to the build directory
139    RenderGrafanaTemplates,
140    /// Deploying Grafana provisioning configuration to the remote host via Ansible
141    DeployGrafanaProvisioning,
142    /// Creating `MySQL` storage directories on remote host
143    CreateMysqlStorage,
144    /// Rendering Backup configuration templates to the build directory
145    RenderBackupTemplates,
146    /// Creating Backup storage directories on remote host
147    CreateBackupStorage,
148    /// Deploying Backup configuration to the remote host via Ansible
149    DeployBackupConfigToRemote,
150    /// Installing backup crontab and maintenance script
151    InstallBackupCrontab,
152    /// Rendering Caddy configuration templates to the build directory
153    RenderCaddyTemplates,
154    /// Deploying Caddy configuration to the remote host via Ansible
155    DeployCaddyConfigToRemote,
156    /// Rendering Docker Compose templates to the build directory
157    RenderDockerComposeTemplates,
158    /// Deploying compose files to the remote host via Ansible
159    DeployComposeFilesToRemote,
160}
161
162impl fmt::Display for ReleaseWorkflowStep {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        let name = match self {
165            Self::CreateTrackerStorage => "Create Tracker Storage",
166            Self::InitTrackerDatabase => "Initialize Tracker Database",
167            Self::RenderTrackerTemplates => "Render Tracker Templates",
168            Self::DeployTrackerConfigToRemote => "Deploy Tracker Config to Remote",
169            Self::CreatePrometheusStorage => "Create Prometheus Storage",
170            Self::RenderPrometheusTemplates => "Render Prometheus Templates",
171            Self::DeployPrometheusConfigToRemote => "Deploy Prometheus Config to Remote",
172            Self::CreateGrafanaStorage => "Create Grafana Storage",
173            Self::RenderGrafanaTemplates => "Render Grafana Templates",
174            Self::DeployGrafanaProvisioning => "Deploy Grafana Provisioning",
175            Self::CreateMysqlStorage => "Create MySQL Storage",
176            Self::RenderBackupTemplates => "Render Backup Templates",
177            Self::CreateBackupStorage => "Create Backup Storage",
178            Self::DeployBackupConfigToRemote => "Deploy Backup Config to Remote",
179            Self::InstallBackupCrontab => "Install Backup Crontab",
180            Self::RenderCaddyTemplates => "Render Caddy Templates",
181            Self::DeployCaddyConfigToRemote => "Deploy Caddy Config to Remote",
182            Self::RenderDockerComposeTemplates => "Render Docker Compose Templates",
183            Self::DeployComposeFilesToRemote => "Deploy Compose Files to Remote",
184        };
185        write!(f, "{name}")
186    }
187}
188
189impl From<crate::domain::environment::state::ReleaseStep> for ReleaseWorkflowStep {
190    fn from(s: crate::domain::environment::state::ReleaseStep) -> Self {
191        use crate::domain::environment::state::ReleaseStep;
192        match s {
193            ReleaseStep::CreateTrackerStorage => Self::CreateTrackerStorage,
194            ReleaseStep::InitTrackerDatabase => Self::InitTrackerDatabase,
195            ReleaseStep::RenderTrackerTemplates => Self::RenderTrackerTemplates,
196            ReleaseStep::DeployTrackerConfigToRemote => Self::DeployTrackerConfigToRemote,
197            ReleaseStep::CreatePrometheusStorage => Self::CreatePrometheusStorage,
198            ReleaseStep::RenderPrometheusTemplates => Self::RenderPrometheusTemplates,
199            ReleaseStep::DeployPrometheusConfigToRemote => Self::DeployPrometheusConfigToRemote,
200            ReleaseStep::CreateGrafanaStorage => Self::CreateGrafanaStorage,
201            ReleaseStep::RenderGrafanaTemplates => Self::RenderGrafanaTemplates,
202            ReleaseStep::DeployGrafanaProvisioning => Self::DeployGrafanaProvisioning,
203            ReleaseStep::CreateMysqlStorage => Self::CreateMysqlStorage,
204            ReleaseStep::RenderBackupTemplates => Self::RenderBackupTemplates,
205            ReleaseStep::CreateBackupStorage => Self::CreateBackupStorage,
206            ReleaseStep::DeployBackupConfigToRemote => Self::DeployBackupConfigToRemote,
207            ReleaseStep::InstallBackupCrontab => Self::InstallBackupCrontab,
208            ReleaseStep::RenderCaddyTemplates => Self::RenderCaddyTemplates,
209            ReleaseStep::DeployCaddyConfigToRemote => Self::DeployCaddyConfigToRemote,
210            ReleaseStep::RenderDockerComposeTemplates => Self::RenderDockerComposeTemplates,
211            ReleaseStep::DeployComposeFilesToRemote => Self::DeployComposeFilesToRemote,
212        }
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn it_should_display_not_found_persistence_error() {
222        let err = PersistenceError::NotFound;
223        assert_eq!(err.to_string(), "Environment not found");
224    }
225
226    #[test]
227    fn it_should_display_conflict_persistence_error() {
228        let err = PersistenceError::Conflict;
229        assert_eq!(
230            err.to_string(),
231            "Conflict: another process is accessing this environment"
232        );
233    }
234
235    #[test]
236    fn it_should_display_invalid_state_error() {
237        let err = InvalidStateError {
238            expected: "provisioned".to_string(),
239            actual: "created".to_string(),
240        };
241        assert_eq!(
242            err.to_string(),
243            "Expected state 'provisioned', but found 'created'"
244        );
245    }
246
247    #[test]
248    fn it_should_convert_from_repository_error_not_found() {
249        use crate::domain::environment::repository::RepositoryError;
250        let domain_err = RepositoryError::NotFound;
251        let app_err = PersistenceError::from(domain_err);
252        assert!(matches!(app_err, PersistenceError::NotFound));
253    }
254
255    #[test]
256    fn it_should_convert_from_repository_error_conflict() {
257        use crate::domain::environment::repository::RepositoryError;
258        let domain_err = RepositoryError::Conflict;
259        let app_err = PersistenceError::from(domain_err);
260        assert!(matches!(app_err, PersistenceError::Conflict));
261    }
262
263    #[test]
264    fn it_should_convert_from_state_type_error() {
265        use crate::domain::environment::state::StateTypeError;
266        let domain_err = StateTypeError::UnexpectedState {
267            expected: "provisioned",
268            actual: "created".to_string(),
269        };
270        let app_err = InvalidStateError::from(domain_err);
271        assert_eq!(app_err.expected, "provisioned");
272        assert_eq!(app_err.actual, "created");
273    }
274
275    #[test]
276    fn it_should_convert_release_step_to_workflow_step() {
277        use crate::domain::environment::state::ReleaseStep;
278        let step = ReleaseStep::InitTrackerDatabase;
279        let ws: ReleaseWorkflowStep = step.into();
280        assert_eq!(ws, ReleaseWorkflowStep::InitTrackerDatabase);
281        assert_eq!(ws.to_string(), "Initialize Tracker Database");
282    }
283}