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 pub fn new(value: impl Into<String>) -> Result<Part, ErnError> {
42 let value = value.into();
43
44 if value.contains(':') || value.contains('/') {
46 return Err(ErnError::InvalidPartFormat);
47 }
48
49 if value.is_empty() {
51 return Err(ErnError::ParseFailure(
52 "Part",
53 "cannot be empty".to_string(),
54 ));
55 }
56
57 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 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#[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}