rustant_tools/
http_api.rs1use async_trait::async_trait;
4use rustant_core::error::ToolError;
5use rustant_core::types::{RiskLevel, ToolOutput};
6use serde_json::{Value, json};
7use std::time::Duration;
8
9use crate::registry::Tool;
10
11pub struct HttpApiTool;
12
13impl Default for HttpApiTool {
14 fn default() -> Self {
15 Self
16 }
17}
18
19impl HttpApiTool {
20 pub fn new() -> Self {
21 Self
22 }
23}
24
25#[async_trait]
26impl Tool for HttpApiTool {
27 fn name(&self) -> &str {
28 "http_api"
29 }
30 fn description(&self) -> &str {
31 "Make HTTP API requests. Actions: get, post, put, delete. Returns status code and response body."
32 }
33 fn parameters_schema(&self) -> Value {
34 json!({
35 "type": "object",
36 "properties": {
37 "action": {
38 "type": "string",
39 "enum": ["get", "post", "put", "delete"],
40 "description": "HTTP method"
41 },
42 "url": { "type": "string", "description": "Request URL" },
43 "body": { "type": "string", "description": "Request body (JSON string for post/put)" },
44 "headers": {
45 "type": "object",
46 "description": "Custom headers as key-value pairs",
47 "additionalProperties": { "type": "string" }
48 }
49 },
50 "required": ["action", "url"]
51 })
52 }
53 fn risk_level(&self) -> RiskLevel {
54 RiskLevel::Execute
55 }
56 fn timeout(&self) -> Duration {
57 Duration::from_secs(30)
58 }
59
60 async fn execute(&self, args: Value) -> Result<ToolOutput, ToolError> {
61 let action = args.get("action").and_then(|v| v.as_str()).unwrap_or("get");
62 let url = args.get("url").and_then(|v| v.as_str()).unwrap_or("");
63 if url.is_empty() {
64 return Ok(ToolOutput::text("Please provide a URL."));
65 }
66
67 let client = reqwest::Client::builder()
68 .timeout(Duration::from_secs(25))
69 .build()
70 .map_err(|e| ToolError::ExecutionFailed {
71 name: "http_api".into(),
72 message: format!("Failed to create HTTP client: {}", e),
73 })?;
74
75 let mut builder = match action {
76 "get" => client.get(url),
77 "post" => client.post(url),
78 "put" => client.put(url),
79 "delete" => client.delete(url),
80 _ => return Ok(ToolOutput::text(format!("Unknown method: {}", action))),
81 };
82
83 if let Some(headers) = args.get("headers").and_then(|v| v.as_object()) {
85 for (key, value) in headers {
86 if let Some(val_str) = value.as_str() {
87 builder = builder.header(key.as_str(), val_str);
88 }
89 }
90 }
91
92 if let Some(body) = args.get("body").and_then(|v| v.as_str()) {
94 builder = builder
95 .header("Content-Type", "application/json")
96 .body(body.to_string());
97 }
98
99 let response = builder
100 .send()
101 .await
102 .map_err(|e| ToolError::ExecutionFailed {
103 name: "http_api".into(),
104 message: format!("HTTP request failed: {}", e),
105 })?;
106
107 let status = response.status();
108 let headers_str: Vec<String> = response
109 .headers()
110 .iter()
111 .take(10)
112 .map(|(k, v)| format!(" {}: {}", k, v.to_str().unwrap_or("?")))
113 .collect();
114 let body = response
115 .text()
116 .await
117 .unwrap_or_else(|_| "<binary>".to_string());
118
119 let body_display = if body.len() > 5000 {
121 format!(
122 "{}...\n(truncated, {} bytes total)",
123 &body[..5000],
124 body.len()
125 )
126 } else {
127 body
128 };
129
130 Ok(ToolOutput::text(format!(
131 "HTTP {} {} → {}\nHeaders:\n{}\nBody:\n{}",
132 action.to_uppercase(),
133 url,
134 status,
135 headers_str.join("\n"),
136 body_display
137 )))
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[tokio::test]
146 async fn test_http_api_missing_url() {
147 let tool = HttpApiTool::new();
148 let result = tool
149 .execute(json!({"action": "get", "url": ""}))
150 .await
151 .unwrap();
152 assert!(result.content.contains("provide a URL"));
153 }
154
155 #[tokio::test]
156 async fn test_http_api_schema() {
157 let tool = HttpApiTool::new();
158 assert_eq!(tool.name(), "http_api");
159 assert_eq!(tool.risk_level(), RiskLevel::Execute);
160 let schema = tool.parameters_schema();
161 assert!(schema["properties"]["action"]["enum"].is_array());
162 }
163
164 #[tokio::test]
165 async fn test_http_api_invalid_url() {
166 let tool = HttpApiTool::new();
167 let result = tool
168 .execute(json!({"action": "get", "url": "not-a-url"}))
169 .await;
170 assert!(result.is_err() || result.unwrap().content.contains("failed"));
172 }
173}