Skip to main content

use_ai_role/
lib.rs

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}