1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7pub mod prelude {
8 pub use crate::{
9 AiInstructionAuthority, AiParticipantId, AiParticipantKind, AiPersonaKind, AiRoleError,
10 AiRoleName, AiRoleScope, AiRoleStatus,
11 };
12}
13
14macro_rules! role_text_newtype {
15 ($name:ident) => {
16 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
17 pub struct $name(String);
18
19 impl $name {
20 pub fn new(value: impl AsRef<str>) -> Result<Self, AiRoleError> {
21 non_empty_text(value).map(Self)
22 }
23
24 pub fn as_str(&self) -> &str {
25 &self.0
26 }
27
28 pub fn value(&self) -> &str {
29 self.as_str()
30 }
31
32 pub fn into_string(self) -> String {
33 self.0
34 }
35 }
36
37 impl AsRef<str> for $name {
38 fn as_ref(&self) -> &str {
39 self.as_str()
40 }
41 }
42
43 impl fmt::Display for $name {
44 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
45 formatter.write_str(self.as_str())
46 }
47 }
48
49 impl FromStr for $name {
50 type Err = AiRoleError;
51
52 fn from_str(value: &str) -> Result<Self, Self::Err> {
53 Self::new(value)
54 }
55 }
56
57 impl TryFrom<&str> for $name {
58 type Error = AiRoleError;
59
60 fn try_from(value: &str) -> Result<Self, Self::Error> {
61 Self::new(value)
62 }
63 }
64 };
65}
66
67macro_rules! role_enum {
68 ($name:ident { $($variant:ident => $label:literal),+ $(,)? }) => {
69 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
70 pub enum $name {
71 $($variant),+
72 }
73
74 impl $name {
75 pub const ALL: &'static [Self] = &[$(Self::$variant),+];
76
77 pub const fn as_str(self) -> &'static str {
78 match self {
79 $(Self::$variant => $label),+
80 }
81 }
82 }
83
84 impl fmt::Display for $name {
85 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
86 formatter.write_str(self.as_str())
87 }
88 }
89
90 impl FromStr for $name {
91 type Err = AiRoleError;
92
93 fn from_str(value: &str) -> Result<Self, Self::Err> {
94 match normalized_label(value)?.as_str() {
95 $($label => Ok(Self::$variant),)+
96 _ => Err(AiRoleError::UnknownLabel),
97 }
98 }
99 }
100 };
101}
102
103role_text_newtype!(AiRoleName);
104role_text_newtype!(AiParticipantId);
105
106role_enum!(AiParticipantKind {
107 User => "user",
108 Assistant => "assistant",
109 System => "system",
110 Developer => "developer",
111 Tool => "tool",
112 Agent => "agent",
113 Evaluator => "evaluator",
114 Observer => "observer",
115 Unknown => "unknown",
116});
117
118role_enum!(AiInstructionAuthority {
119 System => "system",
120 Developer => "developer",
121 User => "user",
122 Tool => "tool",
123 Retrieved => "retrieved",
124 Memory => "memory",
125 Unknown => "unknown",
126});
127
128role_enum!(AiRoleScope {
129 Conversation => "conversation",
130 Session => "session",
131 Task => "task",
132 Tool => "tool",
133 Agent => "agent",
134 Organization => "organization",
135 Global => "global",
136});
137
138role_enum!(AiPersonaKind {
139 Assistant => "assistant",
140 Critic => "critic",
141 Planner => "planner",
142 Researcher => "researcher",
143 Coder => "coder",
144 Reviewer => "reviewer",
145 Tutor => "tutor",
146 Analyst => "analyst",
147 Operator => "operator",
148 Custom => "custom",
149});
150
151role_enum!(AiRoleStatus {
152 Active => "active",
153 Inactive => "inactive",
154 Deprecated => "deprecated",
155 Experimental => "experimental",
156});
157
158#[derive(Clone, Copy, Debug, Eq, PartialEq)]
159pub enum AiRoleError {
160 Empty,
161 UnknownLabel,
162}
163
164impl fmt::Display for AiRoleError {
165 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
166 match self {
167 Self::Empty => formatter.write_str("AI role metadata text cannot be empty"),
168 Self::UnknownLabel => formatter.write_str("unknown AI role metadata label"),
169 }
170 }
171}
172
173impl Error for AiRoleError {}
174
175fn non_empty_text(value: impl AsRef<str>) -> Result<String, AiRoleError> {
176 let trimmed = value.as_ref().trim();
177 if trimmed.is_empty() {
178 Err(AiRoleError::Empty)
179 } else {
180 Ok(trimmed.to_string())
181 }
182}
183
184fn normalized_label(value: &str) -> Result<String, AiRoleError> {
185 let trimmed = value.trim();
186 if trimmed.is_empty() {
187 Err(AiRoleError::Empty)
188 } else {
189 Ok(trimmed.to_ascii_lowercase().replace(['_', ' '], "-"))
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::{
196 AiInstructionAuthority, AiParticipantId, AiParticipantKind, AiPersonaKind, AiRoleError,
197 AiRoleName, AiRoleScope, AiRoleStatus,
198 };
199 use core::{fmt, str::FromStr};
200
201 macro_rules! assert_text_newtype {
202 ($type:ty, $value:literal) => {{
203 let value = <$type>::new(concat!(" ", $value, " "))?;
204 assert_eq!(value.as_str(), $value);
205 assert_eq!(value.value(), $value);
206 assert_eq!(value.as_ref(), $value);
207 assert_eq!(value.to_string(), $value);
208 assert_eq!(<$type as TryFrom<&str>>::try_from($value)?, value);
209 assert_eq!(value.into_string(), $value.to_string());
210 }};
211 }
212
213 fn assert_enum_family<T>(variants: &[T]) -> Result<(), AiRoleError>
214 where
215 T: Copy + Eq + fmt::Debug + fmt::Display + FromStr<Err = AiRoleError>,
216 {
217 for variant in variants {
218 let label = variant.to_string();
219 assert_eq!(label.parse::<T>()?, *variant);
220 assert_eq!(label.replace('-', "_").parse::<T>()?, *variant);
221 assert_eq!(label.replace('-', " ").parse::<T>()?, *variant);
222 }
223 Ok(())
224 }
225
226 #[test]
227 fn validates_role_text_newtypes() -> Result<(), AiRoleError> {
228 assert_text_newtype!(AiRoleName, "assistant");
229 assert_text_newtype!(AiParticipantId, "participant-001");
230 assert_eq!(AiRoleName::new(" "), Err(AiRoleError::Empty));
231 Ok(())
232 }
233
234 #[test]
235 fn displays_and_parses_role_enums() -> Result<(), AiRoleError> {
236 assert_enum_family(AiParticipantKind::ALL)?;
237 assert_enum_family(AiInstructionAuthority::ALL)?;
238 assert_enum_family(AiRoleScope::ALL)?;
239 assert_enum_family(AiPersonaKind::ALL)?;
240 assert_enum_family(AiRoleStatus::ALL)?;
241 assert_eq!(
242 "system".parse::<AiInstructionAuthority>()?,
243 AiInstructionAuthority::System
244 );
245 Ok(())
246 }
247}