1use std::fmt;
4use std::str::FromStr;
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub enum TypeClass {
12 Llm,
14 Rule,
16 Human,
18 Composite,
20 Sensor,
22 Actuator,
24 Hybrid,
26 Extension(ExtensionClass),
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32pub struct ExtensionClass(String);
33
34impl ExtensionClass {
35 pub fn new(name: &str) -> Result<Self, &'static str> {
42 if name.is_empty() {
43 return Err("extension class name cannot be empty");
44 }
45 if name.len() < 2 {
46 return Err("extension class name must be at least 2 characters");
47 }
48 if !name.chars().all(|c| c.is_ascii_lowercase()) {
49 return Err("extension class name must be all lowercase letters");
50 }
51 Ok(Self(name.to_string()))
52 }
53
54 #[must_use]
56 pub fn as_str(&self) -> &str {
57 &self.0
58 }
59}
60
61impl fmt::Display for ExtensionClass {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 write!(f, "{}", self.0)
64 }
65}
66
67impl TypeClass {
68 #[must_use]
70 pub fn as_str(&self) -> &str {
71 match self {
72 Self::Llm => "llm",
73 Self::Rule => "rule",
74 Self::Human => "human",
75 Self::Composite => "composite",
76 Self::Sensor => "sensor",
77 Self::Actuator => "actuator",
78 Self::Hybrid => "hybrid",
79 Self::Extension(ext) => ext.as_str(),
80 }
81 }
82
83 #[must_use]
85 pub const fn is_core(&self) -> bool {
86 !matches!(self, Self::Extension(_))
87 }
88}
89
90impl fmt::Display for TypeClass {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 write!(f, "{}", self.as_str())
93 }
94}
95
96impl FromStr for TypeClass {
97 type Err = &'static str;
98
99 fn from_str(s: &str) -> Result<Self, Self::Err> {
100 match s {
101 "llm" => Ok(Self::Llm),
102 "rule" => Ok(Self::Rule),
103 "human" => Ok(Self::Human),
104 "composite" => Ok(Self::Composite),
105 "sensor" => Ok(Self::Sensor),
106 "actuator" => Ok(Self::Actuator),
107 "hybrid" => Ok(Self::Hybrid),
108 other => {
109 let ext = ExtensionClass::new(other)?;
110 Ok(Self::Extension(ext))
111 }
112 }
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn parse_core_classes() {
122 assert_eq!("llm".parse::<TypeClass>().unwrap(), TypeClass::Llm);
123 assert_eq!("rule".parse::<TypeClass>().unwrap(), TypeClass::Rule);
124 assert_eq!("human".parse::<TypeClass>().unwrap(), TypeClass::Human);
125 assert_eq!("composite".parse::<TypeClass>().unwrap(), TypeClass::Composite);
126 assert_eq!("sensor".parse::<TypeClass>().unwrap(), TypeClass::Sensor);
127 assert_eq!("actuator".parse::<TypeClass>().unwrap(), TypeClass::Actuator);
128 assert_eq!("hybrid".parse::<TypeClass>().unwrap(), TypeClass::Hybrid);
129 }
130
131 #[test]
132 fn core_classes_are_core() {
133 assert!(TypeClass::Llm.is_core());
134 assert!(TypeClass::Rule.is_core());
135 assert!(TypeClass::Human.is_core());
136 }
137
138 #[test]
139 fn parse_extension_class() {
140 let tc = "custom".parse::<TypeClass>().unwrap();
141 assert!(matches!(tc, TypeClass::Extension(_)));
142 assert!(!tc.is_core());
143 assert_eq!(tc.as_str(), "custom");
144 }
145
146 #[test]
147 fn extension_class_validation() {
148 assert!(ExtensionClass::new("a").is_err());
150 assert!(ExtensionClass::new("").is_err());
152 assert!(ExtensionClass::new("Custom").is_err());
154 assert!(ExtensionClass::new("v2").is_err());
156 assert!(ExtensionClass::new("custom").is_ok());
158 }
159}