acton_ern/model/
part.rs

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