Skip to main content

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!(
62                    "length exceeds maximum of 63 characters (got {})",
63                    value.len()
64                ),
65            ));
66        }
67
68        // Check for valid characters
69        let valid_chars = value
70            .chars()
71            .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.');
72
73        if !valid_chars {
74            return Err(ErnError::ParseFailure(
75                "Part",
76                "can only contain alphanumeric characters, hyphens, underscores, and dots"
77                    .to_string(),
78            ));
79        }
80
81        Ok(Part(value))
82    }
83}
84
85impl fmt::Display for Part {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(f, "{}", self.0)
88    }
89}
90
91impl std::str::FromStr for Part {
92    type Err = ErnError;
93    fn from_str(s: &str) -> Result<Self, Self::Err> {
94        Part::new(Cow::Owned(s.to_owned()))
95    }
96}
97
98// impl From<Part> for String {
99//     fn from(part: Part) -> Self {
100//         part.0
101//     }
102// }
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn test_part_creation() -> anyhow::Result<()> {
110        let part = Part::new("segment")?;
111        assert_eq!(part.as_str(), "segment");
112        Ok(())
113    }
114
115    #[test]
116    fn test_part_display() -> anyhow::Result<()> {
117        let part = Part::new("example")?;
118        assert_eq!(format!("{}", part), "example");
119        Ok(())
120    }
121
122    #[test]
123    fn test_part_from_str() {
124        let part: Part = "test".parse().unwrap();
125        assert_eq!(part.as_str(), "test");
126    }
127
128    #[test]
129    fn test_part_equality() -> anyhow::Result<()> {
130        let part1 = Part::new("segment1")?;
131        let part2 = Part::new("segment1")?;
132        let part3 = Part::new("segment2")?;
133        assert_eq!(part1, part2);
134        assert_ne!(part1, part3);
135        Ok(())
136    }
137
138    #[test]
139    fn test_part_into_string() -> anyhow::Result<()> {
140        let part = Part::new("segment")?;
141        let string: String = part.into();
142        assert_eq!(string, "segment");
143        Ok(())
144    }
145    #[test]
146    fn test_part_validation_too_long() {
147        let long_part = "a".repeat(64);
148        let result = Part::new(long_part);
149        assert!(result.is_err());
150        match result {
151            Err(ErnError::ParseFailure(component, msg)) => {
152                assert_eq!(component, "Part");
153                assert!(msg.contains("length exceeds maximum"));
154            }
155            _ => panic!("Expected ParseFailure error for too long part"),
156        }
157    }
158
159    #[test]
160    fn test_part_validation_invalid_chars() {
161        let result = Part::new("invalid*part");
162        assert!(result.is_err());
163        match result {
164            Err(ErnError::ParseFailure(component, msg)) => {
165                assert_eq!(component, "Part");
166                assert!(msg.contains("can only contain"));
167            }
168            _ => panic!("Expected ParseFailure error for invalid characters"),
169        }
170    }
171
172    #[test]
173    fn test_part_validation_reserved_chars() {
174        let result1 = Part::new("invalid:part");
175        let result2 = Part::new("invalid/part");
176
177        assert!(result1.is_err());
178        assert!(result2.is_err());
179
180        match result1 {
181            Err(ErnError::InvalidPartFormat) => {}
182            _ => panic!("Expected InvalidPartFormat error for part with ':'"),
183        }
184
185        match result2 {
186            Err(ErnError::InvalidPartFormat) => {}
187            _ => panic!("Expected InvalidPartFormat error for part with '/'"),
188        }
189    }
190
191    #[test]
192    fn test_part_validation_valid_complex() -> anyhow::Result<()> {
193        let result = Part::new("valid-part_123.test")?;
194        assert_eq!(result.as_str(), "valid-part_123.test");
195        Ok(())
196    }
197}