Skip to content

Commit 886f2a5

Browse files
committed
refactor: [#281] replace to_* methods with TryFrom trait for DTO conversions
- Convert GrafanaSection, PrometheusSection, SshCredentialsConfig, DatabaseSection, TrackerCoreSection, TrackerSection, and ProviderSection to use TryFrom trait for domain type conversion - Update environment_config.rs to use try_into() for all conversions - Remove old to_* methods (no backward compatibility needed pre-v1.0) Follows pattern documented in docs/decisions/tryfrom-for-dto-to-domain-conversion.md
1 parent 4dd3e37 commit 886f2a5

7 files changed

Lines changed: 157 additions & 200 deletions

File tree

src/application/command_handlers/create/config/environment_config.rs

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
//! all configuration needed to create a deployment environment. It handles
55
//! deserialization from configuration sources and conversion to domain types.
66
7+
use std::convert::TryInto;
8+
79
use schemars::JsonSchema;
810
use serde::{Deserialize, Serialize};
911

@@ -327,27 +329,21 @@ impl EnvironmentCreationConfig {
327329
};
328330

329331
// Convert ProviderSection (DTO) to domain ProviderConfig (validates profile_name, etc.)
330-
let provider_config = self.provider.to_provider_config()?;
332+
let provider_config = self.provider.try_into()?;
331333

332334
// Get SSH port before consuming ssh_credentials
333335
let ssh_port = self.ssh_credentials.port;
334336

335337
// Convert SSH credentials config to domain type
336-
let ssh_credentials = self.ssh_credentials.to_ssh_credentials()?;
338+
let ssh_credentials = self.ssh_credentials.try_into()?;
337339

338340
// Convert TrackerSection (DTO) to domain TrackerConfig (validates bind addresses, etc.)
339-
let tracker_config = self.tracker.to_tracker_config()?;
341+
let tracker_config = self.tracker.try_into()?;
340342

341343
// Convert Prometheus and Grafana sections to domain types
342-
let prometheus_config = self
343-
.prometheus
344-
.map(|section| section.to_prometheus_config())
345-
.transpose()?;
344+
let prometheus_config = self.prometheus.map(TryInto::try_into).transpose()?;
346345

347-
let grafana_config = self
348-
.grafana
349-
.map(|section| section.to_grafana_config())
350-
.transpose()?;
346+
let grafana_config = self.grafana.map(TryInto::try_into).transpose()?;
351347

352348
// Note: Grafana-Prometheus dependency is now validated at domain level
353349
// in UserInputs::with_tracker() when the environment is created

src/application/command_handlers/create/config/grafana.rs

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
//! This module contains the DTO type for Grafana configuration used in
44
//! environment creation. This type uses raw primitives (String) for JSON
55
//! deserialization and converts to the rich domain type (`GrafanaConfig`).
6+
//!
7+
//! It follows the **`TryFrom` pattern** for DTO to domain conversion, delegating
8+
//! all business validation to the domain layer.
9+
//!
10+
//! See `docs/decisions/tryfrom-for-dto-to-domain-conversion.md` for rationale.
11+
12+
use std::convert::TryFrom;
613

714
use schemars::JsonSchema;
815
use serde::{Deserialize, Serialize};
@@ -82,23 +89,20 @@ impl Default for GrafanaSection {
8289
}
8390
}
8491

85-
impl GrafanaSection {
86-
/// Converts this DTO to a domain `GrafanaConfig`
87-
///
88-
/// This method performs validation and type conversion from the
89-
/// string-based DTO to the strongly-typed domain model with secrecy
90-
/// protection for the password.
91-
///
92-
/// # Errors
93-
///
94-
/// Returns `CreateConfigError::InvalidDomain` if the domain is invalid.
95-
/// Returns `CreateConfigError::TlsProxyWithoutDomain` if `use_tls_proxy`
96-
/// is true but no domain is provided.
97-
pub fn to_grafana_config(&self) -> Result<GrafanaConfig, CreateConfigError> {
98-
let use_tls_proxy = self.use_tls_proxy.unwrap_or(false);
92+
/// Converts from application DTO to domain type using `TryFrom` trait
93+
///
94+
/// This follows the idiomatic Rust pattern for fallible type
95+
/// conversions, enabling use of `.try_into()` and `TryFrom::try_from()`.
96+
///
97+
/// See `docs/decisions/tryfrom-for-dto-to-domain-conversion.md` for rationale.
98+
impl TryFrom<GrafanaSection> for GrafanaConfig {
99+
type Error = CreateConfigError;
100+
101+
fn try_from(section: GrafanaSection) -> Result<Self, Self::Error> {
102+
let use_tls_proxy = section.use_tls_proxy.unwrap_or(false);
99103

100104
// Validate: use_tls_proxy requires domain
101-
if use_tls_proxy && self.domain.is_none() {
105+
if use_tls_proxy && section.domain.is_none() {
102106
return Err(CreateConfigError::TlsProxyWithoutDomain {
103107
service_type: "Grafana".to_string(),
104108
bind_address: "N/A (hardcoded port 3000)".to_string(),
@@ -107,7 +111,7 @@ impl GrafanaSection {
107111

108112
// Parse domain if present
109113
let domain =
110-
match &self.domain {
114+
match &section.domain {
111115
Some(domain_str) => Some(DomainName::new(domain_str).map_err(|e| {
112116
CreateConfigError::InvalidDomain {
113117
domain: domain_str.clone(),
@@ -118,8 +122,8 @@ impl GrafanaSection {
118122
};
119123

120124
Ok(GrafanaConfig::new(
121-
self.admin_user.clone(),
122-
self.admin_password.clone(),
125+
section.admin_user,
126+
section.admin_password,
123127
domain,
124128
use_tls_proxy,
125129
))
@@ -148,7 +152,7 @@ mod tests {
148152
use_tls_proxy: None,
149153
};
150154

151-
let result = section.to_grafana_config();
155+
let result: Result<GrafanaConfig, _> = section.try_into();
152156
assert!(result.is_ok());
153157

154158
let config = result.unwrap();
@@ -159,7 +163,7 @@ mod tests {
159163
#[test]
160164
fn it_should_convert_default_section_to_default_config() {
161165
let section = GrafanaSection::default();
162-
let result = section.to_grafana_config();
166+
let result: Result<GrafanaConfig, _> = section.try_into();
163167
assert!(result.is_ok());
164168

165169
let config = result.unwrap();
@@ -175,7 +179,7 @@ mod tests {
175179
use_tls_proxy: None,
176180
};
177181

178-
let config = section.to_grafana_config().unwrap();
182+
let config: GrafanaConfig = section.try_into().unwrap();
179183
let debug_output = format!("{config:?}");
180184

181185
// Password should be redacted in debug output
@@ -192,7 +196,7 @@ mod tests {
192196
use_tls_proxy: Some(true),
193197
};
194198

195-
let result = section.to_grafana_config();
199+
let result: Result<GrafanaConfig, _> = section.try_into();
196200
assert!(result.is_ok());
197201

198202
let config = result.unwrap();
@@ -209,7 +213,7 @@ mod tests {
209213
use_tls_proxy: Some(false),
210214
};
211215

212-
let result = section.to_grafana_config();
216+
let result: Result<GrafanaConfig, _> = section.try_into();
213217
assert!(result.is_ok());
214218

215219
let config = result.unwrap();
@@ -229,7 +233,7 @@ mod tests {
229233
use_tls_proxy: Some(true),
230234
};
231235

232-
let result = section.to_grafana_config();
236+
let result: Result<GrafanaConfig, CreateConfigError> = section.try_into();
233237
assert!(result.is_err());
234238

235239
let err = result.unwrap_err();
@@ -248,7 +252,7 @@ mod tests {
248252
use_tls_proxy: Some(true),
249253
};
250254

251-
let result = section.to_grafana_config();
255+
let result: Result<GrafanaConfig, CreateConfigError> = section.try_into();
252256
assert!(result.is_err());
253257

254258
let err = result.unwrap_err();

src/application/command_handlers/create/config/prometheus.rs

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
//! This module contains the DTO type for Prometheus configuration used in
44
//! environment creation. This type uses raw primitives (u32) for JSON
55
//! deserialization and converts to the rich domain type (`PrometheusConfig`).
6+
//!
7+
//! # Conversion Pattern
8+
//!
9+
//! Uses `TryFrom` for idiomatic Rust conversion from DTO to domain type.
10+
//! See ADR: `docs/decisions/tryfrom-for-dto-to-domain-conversion.md`
611
12+
use std::convert::TryFrom;
713
use std::num::NonZeroU32;
814

915
use schemars::JsonSchema;
@@ -41,20 +47,14 @@ impl Default for PrometheusSection {
4147
}
4248
}
4349

44-
impl PrometheusSection {
45-
/// Converts this DTO to a domain `PrometheusConfig`
46-
///
47-
/// This method performs validation and type conversion from the
48-
/// u32 DTO to the strongly-typed domain model with `NonZeroU32`.
49-
///
50-
/// # Errors
51-
///
52-
/// Returns error if scrape interval is 0
53-
pub fn to_prometheus_config(&self) -> Result<PrometheusConfig, CreateConfigError> {
54-
let interval = NonZeroU32::new(self.scrape_interval_in_secs).ok_or_else(|| {
50+
impl TryFrom<PrometheusSection> for PrometheusConfig {
51+
type Error = CreateConfigError;
52+
53+
fn try_from(section: PrometheusSection) -> Result<Self, Self::Error> {
54+
let interval = NonZeroU32::new(section.scrape_interval_in_secs).ok_or_else(|| {
5555
CreateConfigError::InvalidPrometheusConfig(format!(
5656
"Scrape interval must be greater than 0, got: {}",
57-
self.scrape_interval_in_secs
57+
section.scrape_interval_in_secs
5858
))
5959
})?;
6060
Ok(PrometheusConfig::new(interval))
@@ -77,14 +77,14 @@ mod tests {
7777
scrape_interval_in_secs: 30,
7878
};
7979

80-
let config = section.to_prometheus_config().expect("Valid config");
80+
let config: PrometheusConfig = section.try_into().expect("Valid config");
8181
assert_eq!(config.scrape_interval_in_secs(), 30);
8282
}
8383

8484
#[test]
8585
fn it_should_convert_default_section_to_default_config() {
8686
let section = PrometheusSection::default();
87-
let config = section.to_prometheus_config().expect("Valid config");
87+
let config: PrometheusConfig = section.try_into().expect("Valid config");
8888

8989
assert_eq!(config, PrometheusConfig::default());
9090
}
@@ -95,7 +95,7 @@ mod tests {
9595
scrape_interval_in_secs: 0,
9696
};
9797

98-
let result = section.to_prometheus_config();
98+
let result: Result<PrometheusConfig, _> = section.try_into();
9999
assert!(result.is_err());
100100
}
101101
}

0 commit comments

Comments
 (0)