acton_ern/model/
category.rs1use std::fmt;
2
3use derive_more::{AsRef, Into};
4use crate::errors::ErnError;
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9#[derive(AsRef, Into, Eq, Debug, PartialEq, Clone, Hash, PartialOrd)]
11#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12pub struct Category(pub(crate) String);
13
14impl Category {
15 pub fn as_str(&self) -> &str {
16 &self.0
17 }
18 pub fn new(value: impl Into<String>) -> Result<Self, ErnError> {
36 let val = value.into();
37
38 if val.is_empty() {
40 return Err(ErnError::ParseFailure("Category", "cannot be empty".to_string()));
41 }
42
43 if val.len() > 63 {
45 return Err(ErnError::ParseFailure(
46 "Category",
47 format!("length exceeds maximum of 63 characters (got {})", val.len())
48 ));
49 }
50
51 let valid_chars = val.chars().all(|c| {
53 c.is_alphanumeric() || c == '-'
54 });
55
56 if !valid_chars {
57 return Err(ErnError::ParseFailure(
58 "Category",
59 "can only contain alphanumeric characters and hyphens".to_string()
60 ));
61 }
62
63 if val.starts_with('-') || val.ends_with('-') {
65 return Err(ErnError::ParseFailure(
66 "Category",
67 "cannot start or end with a hyphen".to_string()
68 ));
69 }
70
71 Ok(Category(val))
72 }
73 pub fn into_owned(self) -> Category {
74 Category(self.0.to_string())
75 }
76}
77
78impl Default for Category {
79 fn default() -> Self {
80 Category("reactive".to_string())
81 }
82}
83
84impl fmt::Display for Category {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 write!(f, "{}", self.0)
87 }
88}
89
90impl std::str::FromStr for Category {
91 type Err = ErnError;
92
93 fn from_str(s: &str) -> Result<Self, Self::Err> {
94 Category::new(s)
95 }
96}
97#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn test_category_creation() -> anyhow::Result<()> {
110 let category = Category::new("test")?;
111 assert_eq!(category.as_str(), "test");
112 Ok(())
113 }
114
115 #[test]
116 fn test_category_default() {
117 let category = Category::default();
118 assert_eq!(category.as_str(), "reactive");
119 }
120
121 #[test]
122 fn test_category_display() -> anyhow::Result<()> {
123 let category = Category::new("example")?;
124 assert_eq!(format!("{}", category), "example");
125 Ok(())
126 }
127
128 #[test]
129 fn test_category_from_str() {
130 let category: Category = "test".parse().unwrap();
131 assert_eq!(category.as_str(), "test");
132 }
133
134 #[test]
135 fn test_category_equality() -> anyhow::Result<()> {
136 let category1 = Category::new("test")?;
137 let category2 = Category::new("test")?;
138 let category3 = Category::new("other")?;
139 assert_eq!(category1, category2);
140 assert_ne!(category1, category3);
141 Ok(())
142 }
143
144 #[test]
145 fn test_category_into_string() -> anyhow::Result<()> {
146 let category = Category::new("test")?;
147 let string: String = category.into();
148 assert_eq!(string, "test");
149 Ok(())
150 }
151
152 #[test]
153 fn test_category_validation_empty() {
154 let result = Category::new("");
155 assert!(result.is_err());
156 match result {
157 Err(ErnError::ParseFailure(component, msg)) => {
158 assert_eq!(component, "Category");
159 assert!(msg.contains("empty"));
160 }
161 _ => panic!("Expected ParseFailure error for empty category"),
162 }
163 }
164
165 #[test]
166 fn test_category_validation_too_long() {
167 let long_category = "a".repeat(64);
168 let result = Category::new(long_category);
169 assert!(result.is_err());
170 match result {
171 Err(ErnError::ParseFailure(component, msg)) => {
172 assert_eq!(component, "Category");
173 assert!(msg.contains("length exceeds maximum"));
174 }
175 _ => panic!("Expected ParseFailure error for too long category"),
176 }
177 }
178
179 #[test]
180 fn test_category_validation_invalid_chars() {
181 let result = Category::new("invalid_category$");
182 assert!(result.is_err());
183 match result {
184 Err(ErnError::ParseFailure(component, msg)) => {
185 assert_eq!(component, "Category");
186 assert!(msg.contains("can only contain"));
187 }
188 _ => panic!("Expected ParseFailure error for invalid characters"),
189 }
190 }
191
192 #[test]
193 fn test_category_validation_hyphen_start_end() {
194 let result1 = Category::new("-invalid");
195 let result2 = Category::new("invalid-");
196
197 assert!(result1.is_err());
198 assert!(result2.is_err());
199
200 match result1 {
201 Err(ErnError::ParseFailure(component, msg)) => {
202 assert_eq!(component, "Category");
203 assert!(msg.contains("cannot start or end with a hyphen"));
204 }
205 _ => panic!("Expected ParseFailure error for category starting with hyphen"),
206 }
207 }
208
209 #[test]
210 fn test_category_validation_valid_complex() -> anyhow::Result<()> {
211 let result = Category::new("valid-category123")?;
212 assert_eq!(result.as_str(), "valid-category123");
213 Ok(())
214 }
215}