agent_skills/
allowed_tools.rs

1//! Allowed tools type for pre-approved tool lists.
2
3use std::fmt;
4
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6
7/// A space-delimited list of pre-approved tools.
8///
9/// This is an experimental feature per the Agent Skills specification.
10///
11/// # Examples
12///
13/// ```
14/// use agent_skills::AllowedTools;
15///
16/// let tools = AllowedTools::new("Bash(git:*) Read Write");
17/// assert_eq!(tools.as_slice().len(), 3);
18/// assert_eq!(tools.as_slice()[0], "Bash(git:*)");
19///
20/// // Empty string creates empty tools
21/// let empty = AllowedTools::new("");
22/// assert!(empty.is_empty());
23/// ```
24#[derive(Debug, Clone, PartialEq, Eq, Default)]
25pub struct AllowedTools(Vec<String>);
26
27impl AllowedTools {
28    /// Creates allowed tools from a space-delimited string.
29    ///
30    /// Empty strings or whitespace-only strings result in an empty list.
31    #[must_use]
32    pub fn new(tools: &str) -> Self {
33        let tools: Vec<String> = tools
34            .split_whitespace()
35            .filter(|s| !s.is_empty())
36            .map(String::from)
37            .collect();
38        Self(tools)
39    }
40
41    /// Creates allowed tools from a vector of strings.
42    #[must_use]
43    pub const fn from_vec(tools: Vec<String>) -> Self {
44        Self(tools)
45    }
46
47    /// Returns the tools as a slice.
48    #[must_use]
49    pub fn as_slice(&self) -> &[String] {
50        &self.0
51    }
52
53    /// Returns `true` if empty.
54    #[must_use]
55    pub const fn is_empty(&self) -> bool {
56        self.0.is_empty()
57    }
58
59    /// Returns the number of tools.
60    #[must_use]
61    pub const fn len(&self) -> usize {
62        self.0.len()
63    }
64
65    /// Returns an iterator over the tools.
66    pub fn iter(&self) -> impl Iterator<Item = &str> {
67        self.0.iter().map(String::as_str)
68    }
69
70    /// Returns `true` if the list contains the specified tool.
71    #[must_use]
72    pub fn contains(&self, tool: &str) -> bool {
73        self.0.iter().any(|t| t == tool)
74    }
75}
76
77impl fmt::Display for AllowedTools {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        write!(f, "{}", self.0.join(" "))
80    }
81}
82
83impl Serialize for AllowedTools {
84    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
85    where
86        S: Serializer,
87    {
88        // Serialize as space-delimited string
89        self.0.join(" ").serialize(serializer)
90    }
91}
92
93impl<'de> Deserialize<'de> for AllowedTools {
94    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
95    where
96        D: Deserializer<'de>,
97    {
98        let s = String::deserialize(deserializer)?;
99        Ok(Self::new(&s))
100    }
101}
102
103impl IntoIterator for AllowedTools {
104    type Item = String;
105    type IntoIter = std::vec::IntoIter<String>;
106
107    fn into_iter(self) -> Self::IntoIter {
108        self.0.into_iter()
109    }
110}
111
112impl<'a> IntoIterator for &'a AllowedTools {
113    type Item = &'a String;
114    type IntoIter = std::slice::Iter<'a, String>;
115
116    fn into_iter(self) -> Self::IntoIter {
117        self.0.iter()
118    }
119}
120
121impl FromIterator<String> for AllowedTools {
122    fn from_iter<T: IntoIterator<Item = String>>(iter: T) -> Self {
123        Self(iter.into_iter().collect())
124    }
125}
126
127#[cfg(test)]
128#[allow(clippy::unwrap_used, clippy::expect_used)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn parses_space_delimited_tools() {
134        let tools = AllowedTools::new("Bash(git:*) Read Write");
135        assert_eq!(tools.as_slice().len(), 3);
136        assert_eq!(tools.as_slice()[0], "Bash(git:*)");
137        assert_eq!(tools.as_slice()[1], "Read");
138        assert_eq!(tools.as_slice()[2], "Write");
139    }
140
141    #[test]
142    fn empty_string_creates_empty_tools() {
143        let tools = AllowedTools::new("");
144        assert!(tools.is_empty());
145        assert_eq!(tools.len(), 0);
146    }
147
148    #[test]
149    fn whitespace_only_creates_empty_tools() {
150        let tools = AllowedTools::new("   \t\n  ");
151        assert!(tools.is_empty());
152    }
153
154    #[test]
155    fn handles_multiple_spaces() {
156        let tools = AllowedTools::new("Read   Write    Execute");
157        assert_eq!(tools.len(), 3);
158    }
159
160    #[test]
161    fn from_vec_works() {
162        let tools = AllowedTools::from_vec(vec!["Read".to_string(), "Write".to_string()]);
163        assert_eq!(tools.len(), 2);
164    }
165
166    #[test]
167    fn iter_works() {
168        let tools = AllowedTools::new("Read Write");
169        let items: Vec<_> = tools.iter().collect();
170        assert_eq!(items, vec!["Read", "Write"]);
171    }
172
173    #[test]
174    fn contains_works() {
175        let tools = AllowedTools::new("Read Write");
176        assert!(tools.contains("Read"));
177        assert!(tools.contains("Write"));
178        assert!(!tools.contains("Execute"));
179    }
180
181    #[test]
182    fn display_works() {
183        let tools = AllowedTools::new("Read Write");
184        assert_eq!(format!("{tools}"), "Read Write");
185    }
186
187    #[test]
188    fn into_iter_works() {
189        let tools = AllowedTools::new("Read Write");
190        let items: Vec<String> = tools.into_iter().collect();
191        assert_eq!(items.len(), 2);
192    }
193
194    #[test]
195    fn ref_into_iter_works() {
196        let tools = AllowedTools::new("Read Write");
197        let items: Vec<_> = (&tools).into_iter().collect();
198        assert_eq!(items.len(), 2);
199    }
200
201    #[test]
202    fn collect_works() {
203        let items = vec!["Read".to_string(), "Write".to_string()];
204        let tools: AllowedTools = items.into_iter().collect();
205        assert_eq!(tools.len(), 2);
206    }
207}