use nanny_core::tool::{Tool, ToolArgs, ToolError, ToolOutput};
use std::io::Read;
use std::time::Duration;
const MAX_BODY_BYTES: u64 = 1024 * 1024;
const HTTP_GET_COST: u64 = 10;
const DEFAULT_TIMEOUT_MS: u64 = 5_000;
pub struct HttpGet {
timeout_ms: u64,
}
impl HttpGet {
pub fn new() -> Self {
Self {
timeout_ms: DEFAULT_TIMEOUT_MS,
}
}
pub fn with_timeout(timeout_ms: u64) -> Self {
Self { timeout_ms }
}
}
impl Default for HttpGet {
fn default() -> Self {
Self::new()
}
}
impl Tool for HttpGet {
fn name(&self) -> &str {
"http_get"
}
fn declared_cost(&self) -> u64 {
HTTP_GET_COST
}
fn execute(&self, args: &ToolArgs) -> Result<ToolOutput, ToolError> {
let url = args.get("url").ok_or_else(|| ToolError::InvalidArgument {
arg: "url".to_string(),
reason: "required argument missing".to_string(),
})?;
if !url.starts_with("http://") && !url.starts_with("https://") {
return Err(ToolError::InvalidArgument {
arg: "url".to_string(),
reason: format!(
"must start with http:// or https://, got: {url}"
),
});
}
let agent = ureq::AgentBuilder::new()
.timeout(Duration::from_millis(self.timeout_ms))
.build();
let response = agent.get(url).call().map_err(|e| match e {
ureq::Error::Status(code, _) => {
ToolError::ExecutionFailed(format!("HTTP {code}"))
}
ureq::Error::Transport(ref t) => {
let msg = t.to_string();
if msg.contains("timed out") || msg.contains("deadline") {
ToolError::Timeout {
timeout_ms: self.timeout_ms,
}
} else {
ToolError::ExecutionFailed(msg)
}
}
})?;
let mut body = String::new();
response
.into_reader()
.take(MAX_BODY_BYTES)
.read_to_string(&mut body)
.map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
Ok(ToolOutput { content: body })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tool() -> HttpGet {
HttpGet::new()
}
#[test]
fn rejects_missing_url() {
let result = tool().execute(&ToolArgs::new());
assert!(matches!(
result,
Err(ToolError::InvalidArgument { ref arg, .. }) if arg == "url"
));
}
#[test]
fn rejects_url_without_scheme() {
let mut args = ToolArgs::new();
args.insert("url".to_string(), "example.com/path".to_string());
let result = tool().execute(&args);
assert!(matches!(
result,
Err(ToolError::InvalidArgument { ref arg, .. }) if arg == "url"
));
}
#[test]
fn rejects_ftp_scheme() {
let mut args = ToolArgs::new();
args.insert("url".to_string(), "ftp://example.com".to_string());
let result = tool().execute(&args);
assert!(matches!(
result,
Err(ToolError::InvalidArgument { ref arg, .. }) if arg == "url"
));
}
#[test]
fn accepts_http_scheme() {
let mut args = ToolArgs::new();
args.insert("url".to_string(), "http://localhost:1/test".to_string());
let result = tool().execute(&args);
assert!(!matches!(result, Err(ToolError::InvalidArgument { .. })));
}
#[test]
fn accepts_https_scheme() {
let mut args = ToolArgs::new();
args.insert("url".to_string(), "https://localhost:1/test".to_string());
let result = tool().execute(&args);
assert!(!matches!(result, Err(ToolError::InvalidArgument { .. })));
}
#[test]
fn declared_cost_is_ten() {
assert_eq!(tool().declared_cost(), 10);
}
#[test]
fn name_is_http_get() {
assert_eq!(tool().name(), "http_get");
}
}