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 ToolArgumentKind, ToolArgumentName, ToolCallError, ToolCallErrorKind, ToolCallId,
10 ToolCallKind, ToolCallStatus, ToolChoiceKind, ToolName, ToolResultKind, ToolSchemaKind,
11 };
12}
13
14macro_rules! tool_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, ToolCallError> {
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 = ToolCallError;
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 = ToolCallError;
59
60 fn try_from(value: &str) -> Result<Self, Self::Error> {
61 Self::new(value)
62 }
63 }
64 };
65}
66
67macro_rules! tool_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 = ToolCallError;
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(ToolCallError::UnknownLabel),
97 }
98 }
99 }
100 };
101}
102
103tool_text_newtype!(ToolName);
104tool_text_newtype!(ToolCallId);
105tool_text_newtype!(ToolArgumentName);
106
107tool_enum!(ToolCallStatus {
108 Pending => "pending",
109 Running => "running",
110 Succeeded => "succeeded",
111 Failed => "failed",
112 Cancelled => "cancelled",
113 TimedOut => "timed-out",
114 Rejected => "rejected",
115});
116
117tool_enum!(ToolCallKind {
118 Function => "function",
119 Api => "api",
120 Search => "search",
121 Database => "database",
122 File => "file",
123 Browser => "browser",
124 Code => "code",
125 Shell => "shell",
126 Calendar => "calendar",
127 Email => "email",
128 Custom => "custom",
129});
130
131tool_enum!(ToolArgumentKind {
132 String => "string",
133 Number => "number",
134 Boolean => "boolean",
135 Json => "json",
136 Array => "array",
137 Object => "object",
138 FileRef => "file-ref",
139 ImageRef => "image-ref",
140 AudioRef => "audio-ref",
141 Custom => "custom",
142});
143
144tool_enum!(ToolResultKind {
145 Text => "text",
146 Json => "json",
147 File => "file",
148 Image => "image",
149 Audio => "audio",
150 Table => "table",
151 Error => "error",
152 Empty => "empty",
153 Custom => "custom",
154});
155
156tool_enum!(ToolSchemaKind {
157 JsonSchema => "json-schema",
158 OpenApi => "open-api",
159 FunctionSignature => "function-signature",
160 Freeform => "freeform",
161 Custom => "custom",
162});
163
164tool_enum!(ToolChoiceKind {
165 None => "none",
166 Auto => "auto",
167 Required => "required",
168 NamedTool => "named-tool",
169 AnyTool => "any-tool",
170});
171
172tool_enum!(ToolCallErrorKind {
173 Validation => "validation",
174 Authorization => "authorization",
175 Timeout => "timeout",
176 NotFound => "not-found",
177 RateLimited => "rate-limited",
178 Execution => "execution",
179 Unknown => "unknown",
180});
181
182#[derive(Clone, Copy, Debug, Eq, PartialEq)]
183pub enum ToolCallError {
184 Empty,
185 UnknownLabel,
186}
187
188impl fmt::Display for ToolCallError {
189 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
190 match self {
191 Self::Empty => formatter.write_str("tool-call metadata text cannot be empty"),
192 Self::UnknownLabel => formatter.write_str("unknown tool-call metadata label"),
193 }
194 }
195}
196
197impl Error for ToolCallError {}
198
199fn non_empty_text(value: impl AsRef<str>) -> Result<String, ToolCallError> {
200 let trimmed = value.as_ref().trim();
201 if trimmed.is_empty() {
202 Err(ToolCallError::Empty)
203 } else {
204 Ok(trimmed.to_string())
205 }
206}
207
208fn normalized_label(value: &str) -> Result<String, ToolCallError> {
209 let trimmed = value.trim();
210 if trimmed.is_empty() {
211 Err(ToolCallError::Empty)
212 } else {
213 Ok(trimmed.to_ascii_lowercase().replace(['_', ' '], "-"))
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::{
220 ToolArgumentKind, ToolArgumentName, ToolCallError, ToolCallErrorKind, ToolCallId,
221 ToolCallKind, ToolCallStatus, ToolChoiceKind, ToolName, ToolResultKind, ToolSchemaKind,
222 };
223 use core::{fmt, str::FromStr};
224
225 macro_rules! assert_text_newtype {
226 ($type:ty, $value:literal) => {{
227 let value = <$type>::new(concat!(" ", $value, " "))?;
228 assert_eq!(value.as_str(), $value);
229 assert_eq!(value.value(), $value);
230 assert_eq!(value.as_ref(), $value);
231 assert_eq!(value.to_string(), $value);
232 assert_eq!(<$type as TryFrom<&str>>::try_from($value)?, value);
233 assert_eq!(value.into_string(), $value.to_string());
234 }};
235 }
236
237 fn assert_enum_family<T>(variants: &[T]) -> Result<(), ToolCallError>
238 where
239 T: Copy + Eq + fmt::Debug + fmt::Display + FromStr<Err = ToolCallError>,
240 {
241 for variant in variants {
242 let label = variant.to_string();
243 assert_eq!(label.parse::<T>()?, *variant);
244 assert_eq!(label.replace('-', "_").parse::<T>()?, *variant);
245 assert_eq!(label.replace('-', " ").parse::<T>()?, *variant);
246 }
247 Ok(())
248 }
249
250 #[test]
251 fn validates_tool_text_newtypes() -> Result<(), ToolCallError> {
252 assert_text_newtype!(ToolName, "ticket-search");
253 assert_text_newtype!(ToolCallId, "call-001");
254 assert_text_newtype!(ToolArgumentName, "query");
255 assert_eq!(ToolName::new(" "), Err(ToolCallError::Empty));
256 Ok(())
257 }
258
259 #[test]
260 fn displays_and_parses_tool_enums() -> Result<(), ToolCallError> {
261 assert_enum_family(ToolCallStatus::ALL)?;
262 assert_enum_family(ToolCallKind::ALL)?;
263 assert_enum_family(ToolArgumentKind::ALL)?;
264 assert_enum_family(ToolResultKind::ALL)?;
265 assert_enum_family(ToolSchemaKind::ALL)?;
266 assert_enum_family(ToolChoiceKind::ALL)?;
267 assert_enum_family(ToolCallErrorKind::ALL)?;
268 assert_eq!(
269 "timed out".parse::<ToolCallStatus>()?,
270 ToolCallStatus::TimedOut
271 );
272 Ok(())
273 }
274}