Skip to main content

use_tool_call/
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        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}