Skip to main content

acton_ern/model/
domain.rs

1use std::fmt;
2
3use derive_more::{AsRef, From, Into};
4
5use crate::errors::ErnError;
6
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9
10#[derive(AsRef, From, Into, Eq, Debug, PartialEq, Clone, Hash, PartialOrd)]
11#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12pub struct Domain(pub(crate) String);
13
14impl Domain {
15    pub fn as_str(&self) -> &str {
16        &self.0
17    }
18
19    pub fn into_owned(self) -> Domain {
20        Domain(self.0)
21    }
22    /// Creates a new Domain with validation.
23    ///
24    /// # Arguments
25    ///
26    /// * `value` - The domain value to validate and create
27    ///
28    /// # Validation Rules
29    ///
30    /// * Domain cannot be empty
31    /// * Domain must be between 1 and 63 characters
32    /// * Domain can only contain alphanumeric characters, hyphens, and dots
33    /// * Domain cannot start or end with a hyphen
34    ///
35    /// # Returns
36    ///
37    /// * `Ok(Domain)` - If validation passes
38    /// * `Err(ErnError)` - If validation fails
39    pub fn new(value: impl Into<String>) -> Result<Self, ErnError> {
40        let val = value.into();
41
42        // Check if empty
43        if val.is_empty() {
44            return Err(ErnError::ParseFailure(
45                "Domain",
46                "cannot be empty".to_string(),
47            ));
48        }
49
50        // Check length
51        if val.len() > 63 {
52            return Err(ErnError::ParseFailure(
53                "Domain",
54                format!(
55                    "length exceeds maximum of 63 characters (got {})",
56                    val.len()
57                ),
58            ));
59        }
60
61        // Check for valid characters
62        let valid_chars = val
63            .chars()
64            .all(|c| c.is_alphanumeric() || c == '-' || c == '.');
65
66        if !valid_chars {
67            return Err(ErnError::ParseFailure(
68                "Domain",
69                "can only contain alphanumeric characters, hyphens, and dots".to_string(),
70            ));
71        }
72
73        // Check if starts or ends with hyphen
74        if val.starts_with('-') || val.ends_with('-') {
75            return Err(ErnError::ParseFailure(
76                "Domain",
77                "cannot start or end with a hyphen".to_string(),
78            ));
79        }
80
81        Ok(Domain(val))
82    }
83}
84
85impl Default for Domain {
86    fn default() -> Self {
87        Domain("acton".to_string())
88    }
89}
90
91impl fmt::Display for Domain {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        write!(f, "{}", self.0)
94    }
95}
96
97impl std::str::FromStr for Domain {
98    type Err = ErnError;
99
100    fn from_str(s: &str) -> Result<Self, Self::Err> {
101        Domain::new(s.to_string())
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_domain_creation() {
111        let domain = Domain::new("test").unwrap();
112        assert_eq!(domain.as_str(), "test");
113    }
114
115    #[test]
116    fn test_domain_default() {
117        let domain = Domain::default();
118        assert_eq!(domain.as_str(), "acton");
119    }
120
121    #[test]
122    fn test_domain_display() {
123        let domain = Domain::new("example").unwrap();
124        assert_eq!(format!("{}", domain), "example");
125    }
126
127    #[test]
128    fn test_domain_from_str() {
129        let domain: Domain = "test".parse().unwrap();
130        assert_eq!(domain.as_str(), "test");
131    }
132
133    #[test]
134    fn test_domain_equality() -> anyhow::Result<()> {
135        let domain1 = Domain::new("test")?;
136        let domain2 = Domain::new("test")?;
137        let domain3 = Domain::new("other")?;
138        assert_eq!(domain1, domain2);
139        assert_ne!(domain1, domain3);
140        Ok(())
141    }
142
143    #[test]
144    fn test_domain_into_string() {
145        let domain = Domain::new("test").unwrap();
146        let string: String = domain.into();
147        assert_eq!(string, "test");
148    }
149
150    #[test]
151    fn test_domain_validation_empty() {
152        let result = Domain::new("");
153        assert!(result.is_err());
154        match result {
155            Err(ErnError::ParseFailure(component, msg)) => {
156                assert_eq!(component, "Domain");
157                assert!(msg.contains("empty"));
158            }
159            _ => panic!("Expected ParseFailure error for empty domain"),
160        }
161    }
162
163    #[test]
164    fn test_domain_validation_too_long() {
165        let long_domain = "a".repeat(64);
166        let result = Domain::new(long_domain);
167        assert!(result.is_err());
168        match result {
169            Err(ErnError::ParseFailure(component, msg)) => {
170                assert_eq!(component, "Domain");
171                assert!(msg.contains("length exceeds maximum"));
172            }
173            _ => panic!("Expected ParseFailure error for too long domain"),
174        }
175    }
176
177    #[test]
178    fn test_domain_validation_invalid_chars() {
179        let result = Domain::new("invalid_domain$");
180        assert!(result.is_err());
181        match result {
182            Err(ErnError::ParseFailure(component, msg)) => {
183                assert_eq!(component, "Domain");
184                assert!(msg.contains("can only contain"));
185            }
186            _ => panic!("Expected ParseFailure error for invalid characters"),
187        }
188    }
189
190    #[test]
191    fn test_domain_validation_hyphen_start_end() {
192        let result1 = Domain::new("-invalid");
193        let result2 = Domain::new("invalid-");
194
195        assert!(result1.is_err());
196        assert!(result2.is_err());
197
198        match result1 {
199            Err(ErnError::ParseFailure(component, msg)) => {
200                assert_eq!(component, "Domain");
201                assert!(msg.contains("cannot start or end with a hyphen"));
202            }
203            _ => panic!("Expected ParseFailure error for domain starting with hyphen"),
204        }
205    }
206
207    #[test]
208    fn test_domain_validation_valid_complex() {
209        let result = Domain::new("valid-domain.name123");
210        assert!(result.is_ok());
211        assert_eq!(result.unwrap().as_str(), "valid-domain.name123");
212    }
213}