1#![doc = include_str!("../README.md")]
2
3mod registry;
6mod result;
7mod schema;
8mod tool;
9
10pub use erio_core::ToolError;
11pub use registry::ToolRegistry;
12pub use result::ToolResult;
13pub use schema::{PropertyType, ToolSchema, ToolSchemaBuilder};
14pub use tool::Tool;
15
16#[cfg(test)]
17mod tests {
18 use super::*;
19 use serde_json::json;
20
21 #[test]
24 fn tool_result_success_creates_success_variant() {
25 let result = ToolResult::success("Hello, world!");
26 assert!(result.is_success());
27 assert_eq!(result.content(), Some("Hello, world!"));
28 }
29
30 #[test]
31 fn tool_result_error_creates_error_variant() {
32 let result = ToolResult::error("Something went wrong");
33 assert!(!result.is_success());
34 assert_eq!(result.error_message(), Some("Something went wrong"));
35 }
36
37 #[test]
38 fn tool_result_serializes_success() {
39 let result = ToolResult::success("output");
40 let json = serde_json::to_value(&result).unwrap();
41 assert_eq!(json["success"], true);
42 assert_eq!(json["content"], "output");
43 }
44
45 #[test]
46 fn tool_result_serializes_error() {
47 let result = ToolResult::error("failed");
48 let json = serde_json::to_value(&result).unwrap();
49 assert_eq!(json["success"], false);
50 assert_eq!(json["error"], "failed");
51 }
52
53 #[test]
56 fn tool_schema_builder_creates_schema() {
57 let schema = ToolSchema::builder()
58 .property("command", PropertyType::String, "The command to run", true)
59 .property(
60 "timeout",
61 PropertyType::Integer,
62 "Timeout in seconds",
63 false,
64 )
65 .build();
66
67 assert!(schema.has_property("command"));
68 assert!(schema.has_property("timeout"));
69 assert!(schema.is_required("command"));
70 assert!(!schema.is_required("timeout"));
71 }
72
73 #[test]
74 fn tool_schema_serializes_to_json_schema() {
75 let schema = ToolSchema::builder()
76 .property("name", PropertyType::String, "The name", true)
77 .build();
78
79 let json = schema.to_json_schema();
80 assert_eq!(json["type"], "object");
81 assert!(json["properties"]["name"].is_object());
82 assert_eq!(json["properties"]["name"]["type"], "string");
83 assert_eq!(json["required"], json!(["name"]));
84 }
85
86 #[test]
87 fn tool_schema_empty_has_no_properties() {
88 let schema = ToolSchema::builder().build();
89 assert_eq!(schema.property_count(), 0);
90 }
91
92 struct EchoTool;
95
96 #[async_trait::async_trait]
97 impl Tool for EchoTool {
98 #[allow(clippy::unnecessary_literal_bound)]
99 fn name(&self) -> &str {
100 "echo"
101 }
102
103 #[allow(clippy::unnecessary_literal_bound)]
104 fn description(&self) -> &str {
105 "Echoes the input message"
106 }
107
108 fn schema(&self) -> ToolSchema {
109 ToolSchema::builder()
110 .property("message", PropertyType::String, "Message to echo", true)
111 .build()
112 }
113
114 async fn execute(&self, params: serde_json::Value) -> Result<ToolResult, ToolError> {
115 let message = params["message"]
116 .as_str()
117 .ok_or_else(|| ToolError::InvalidParams("missing 'message' field".into()))?;
118 Ok(ToolResult::success(message))
119 }
120 }
121
122 #[test]
123 fn tool_returns_name() {
124 let tool = EchoTool;
125 assert_eq!(tool.name(), "echo");
126 }
127
128 #[test]
129 fn tool_returns_description() {
130 let tool = EchoTool;
131 assert_eq!(tool.description(), "Echoes the input message");
132 }
133
134 #[test]
135 fn tool_returns_schema() {
136 let tool = EchoTool;
137 let schema = tool.schema();
138 assert!(schema.has_property("message"));
139 }
140
141 #[tokio::test]
142 async fn tool_executes_successfully() {
143 let tool = EchoTool;
144 let params = json!({"message": "Hello"});
145 let result = tool.execute(params).await.unwrap();
146 assert!(result.is_success());
147 assert_eq!(result.content(), Some("Hello"));
148 }
149
150 #[tokio::test]
151 async fn tool_returns_error_for_invalid_params() {
152 let tool = EchoTool;
153 let params = json!({});
154 let result = tool.execute(params).await;
155 assert!(result.is_err());
156 assert!(matches!(result.unwrap_err(), ToolError::InvalidParams(_)));
157 }
158
159 #[test]
162 fn registry_starts_empty() {
163 let registry = ToolRegistry::new();
164 assert_eq!(registry.len(), 0);
165 assert!(registry.is_empty());
166 }
167
168 #[test]
169 fn registry_registers_tool() {
170 let mut registry = ToolRegistry::new();
171 registry.register(EchoTool);
172 assert_eq!(registry.len(), 1);
173 assert!(!registry.is_empty());
174 }
175
176 #[test]
177 fn registry_gets_tool_by_name() {
178 let mut registry = ToolRegistry::new();
179 registry.register(EchoTool);
180 let tool = registry.get("echo");
181 assert!(tool.is_some());
182 assert_eq!(tool.unwrap().name(), "echo");
183 }
184
185 #[test]
186 fn registry_returns_none_for_unknown_tool() {
187 let registry = ToolRegistry::new();
188 assert!(registry.get("unknown").is_none());
189 }
190
191 #[test]
192 fn registry_lists_all_tools() {
193 let mut registry = ToolRegistry::new();
194 registry.register(EchoTool);
195 let tools = registry.list();
196 assert_eq!(tools.len(), 1);
197 assert_eq!(tools[0], "echo");
198 }
199
200 #[test]
201 fn registry_contains_checks_existence() {
202 let mut registry = ToolRegistry::new();
203 registry.register(EchoTool);
204 assert!(registry.contains("echo"));
205 assert!(!registry.contains("unknown"));
206 }
207
208 #[tokio::test]
209 async fn registry_executes_tool_by_name() {
210 let mut registry = ToolRegistry::new();
211 registry.register(EchoTool);
212 let result = registry.execute("echo", json!({"message": "test"})).await;
213 assert!(result.is_ok());
214 assert_eq!(result.unwrap().content(), Some("test"));
215 }
216
217 #[tokio::test]
218 async fn registry_returns_not_found_for_unknown_tool() {
219 let registry = ToolRegistry::new();
220 let result = registry.execute("unknown", json!({})).await;
221 assert!(result.is_err());
222 assert!(matches!(result.unwrap_err(), ToolError::NotFound(_)));
223 }
224}