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("Domain", "cannot be empty".to_string()));
45        }
46        
47        // Check length
48        if val.len() > 63 {
49            return Err(ErnError::ParseFailure(
50                "Domain",
51                format!("length exceeds maximum of 63 characters (got {})", val.len())
52            ));
53        }
54        
55        // Check for valid characters
56        let valid_chars = val.chars().all(|c| {
57            c.is_alphanumeric() || c == '-' || c == '.'
58        });
59        
60        if !valid_chars {
61            return Err(ErnError::ParseFailure(
62                "Domain",
63                "can only contain alphanumeric characters, hyphens, and dots".to_string()
64            ));
65        }
66        
67        // Check if starts or ends with hyphen
68        if val.starts_with('-') || val.ends_with('-') {
69            return Err(ErnError::ParseFailure(
70                "Domain",
71                "cannot start or end with a hyphen".to_string()
72            ));
73        }
74        
75        Ok(Domain(val))
76    }
77}
78
79impl Default for Domain {
80    fn default() -> Self {
81        Domain("acton".to_string())
82    }
83}
84
85impl fmt::Display for Domain {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(f, "{}", self.0)
88    }
89}
90
91impl std::str::FromStr for Domain {
92    type Err = ErnError;
93
94    fn from_str(s: &str) -> Result<Self, Self::Err> {
95        Domain::new(s.to_string())
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn test_domain_creation() {
105        let domain = Domain::new("test").unwrap();
106        assert_eq!(domain.as_str(), "test");
107    }
108
109    #[test]
110    fn test_domain_default() {
111        let domain = Domain::default();
112        assert_eq!(domain.as_str(), "acton");
113    }
114
115    #[test]
116    fn test_domain_display() {
117        let domain = Domain::new("example").unwrap();
118        assert_eq!(format!("{}", domain), "example");
119    }
120
121    #[test]
122    fn test_domain_from_str() {
123        let domain: Domain = "test".parse().unwrap();
124        assert_eq!(domain.as_str(), "test");
125    }
126
127    #[test]
128    fn test_domain_equality() -> anyhow::Result<()> {
129        let domain1 = Domain::new("test")?;
130        let domain2 = Domain::new("test")?;
131        let domain3 = Domain::new("other")?;
132        assert_eq!(domain1, domain2);
133        assert_ne!(domain1, domain3);
134        Ok(())
135    }
136
137    #[test]
138    fn test_domain_into_string() {
139        let domain = Domain::new("test").unwrap();
140        let string: String = domain.into();
141        assert_eq!(string, "test");
142    }
143    
144    #[test]
145    fn test_domain_validation_empty() {
146        let result = Domain::new("");
147        assert!(result.is_err());
148        match result {
149            Err(ErnError::ParseFailure(component, msg)) => {
150                assert_eq!(component, "Domain");
151                assert!(msg.contains("empty"));
152            }
153            _ => panic!("Expected ParseFailure error for empty domain"),
154        }
155    }
156    
157    #[test]
158    fn test_domain_validation_too_long() {
159        let long_domain = "a".repeat(64);
160        let result = Domain::new(long_domain);
161        assert!(result.is_err());
162        match result {
163            Err(ErnError::ParseFailure(component, msg)) => {
164                assert_eq!(component, "Domain");
165                assert!(msg.contains("length exceeds maximum"));
166            }
167            _ => panic!("Expected ParseFailure error for too long domain"),
168        }
169    }
170    
171    #[test]
172    fn test_domain_validation_invalid_chars() {
173        let result = Domain::new("invalid_domain$");
174        assert!(result.is_err());
175        match result {
176            Err(ErnError::ParseFailure(component, msg)) => {
177                assert_eq!(component, "Domain");
178                assert!(msg.contains("can only contain"));
179            }
180            _ => panic!("Expected ParseFailure error for invalid characters"),
181        }
182    }
183    
184    #[test]
185    fn test_domain_validation_hyphen_start_end() {
186        let result1 = Domain::new("-invalid");
187        let result2 = Domain::new("invalid-");
188        
189        assert!(result1.is_err());
190        assert!(result2.is_err());
191        
192        match result1 {
193            Err(ErnError::ParseFailure(component, msg)) => {
194                assert_eq!(component, "Domain");
195                assert!(msg.contains("cannot start or end with a hyphen"));
196            }
197            _ => panic!("Expected ParseFailure error for domain starting with hyphen"),
198        }
199    }
200    
201    #[test]
202    fn test_domain_validation_valid_complex() {
203        let result = Domain::new("valid-domain.name123");
204        assert!(result.is_ok());
205        assert_eq!(result.unwrap().as_str(), "valid-domain.name123");
206    }
207}