use serde_json::{json, Value};
use super::{external_http_client, extract_str, parse_json_input};
pub(super) fn schemas() -> Vec<Value> {
vec![
json!({
"type": "function",
"function": {
"name": "crate_info",
"description": "Get metadata for a Rust crate on crates.io: latest version, description, downloads, homepage.",
"parameters": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "Crate name (e.g. 'tokio')" }
},
"required": ["name"]
}
}
}),
json!({
"type": "function",
"function": {
"name": "npm_info",
"description": "Get metadata for an npm package: latest version, description, homepage, weekly downloads.",
"parameters": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "Package name (e.g. 'react' or '@scope/pkg')" }
},
"required": ["name"]
}
}
}),
]
}
pub(super) fn dispatch(name: &str, input: &str) -> Option<Result<String, String>> {
let result = match name {
"crate_info" => run_crate_info(input),
"npm_info" => run_npm_info(input),
_ => return None,
};
Some(result)
}
fn run_crate_info(input: &str) -> Result<String, String> {
let v = parse_json_input(input, "crate_info")?;
let name = extract_str(&v, "name", "crate_info")?;
let url = format!("https://crates.io/api/v1/crates/{name}");
let client = external_http_client()?;
let resp = client
.get(&url)
.send()
.map_err(|e| format!("crate_info: request failed: {e}"))?;
let status = resp.status();
if status == reqwest::StatusCode::NOT_FOUND {
return Err(format!("crate_info: no crate named '{name}'"));
}
if !status.is_success() {
return Err(format!("crate_info: HTTP {status}"));
}
let data: Value = resp
.json()
.map_err(|e| format!("crate_info: parse failed: {e}"))?;
let krate = data
.get("crate")
.ok_or("crate_info: response missing 'crate'")?;
Ok(json!({
"name": krate.get("name").and_then(Value::as_str).unwrap_or(name),
"description": krate.get("description").and_then(Value::as_str).unwrap_or(""),
"latest_version": krate.get("max_stable_version").and_then(Value::as_str)
.or_else(|| krate.get("max_version").and_then(Value::as_str))
.unwrap_or(""),
"downloads": krate.get("downloads").and_then(Value::as_u64).unwrap_or(0),
"recent_downloads": krate.get("recent_downloads").and_then(Value::as_u64).unwrap_or(0),
"homepage": krate.get("homepage").and_then(Value::as_str).unwrap_or(""),
"repository": krate.get("repository").and_then(Value::as_str).unwrap_or(""),
"documentation": krate.get("documentation").and_then(Value::as_str).unwrap_or(""),
"updated_at": krate.get("updated_at").and_then(Value::as_str).unwrap_or(""),
})
.to_string())
}
fn run_npm_info(input: &str) -> Result<String, String> {
let v = parse_json_input(input, "npm_info")?;
let name = extract_str(&v, "name", "npm_info")?;
let client = external_http_client()?;
let url = format!("https://registry.npmjs.org/{name}");
let resp = client
.get(&url)
.send()
.map_err(|e| format!("npm_info: request failed: {e}"))?;
let status = resp.status();
if status == reqwest::StatusCode::NOT_FOUND {
return Err(format!("npm_info: no package named '{name}'"));
}
if !status.is_success() {
return Err(format!("npm_info: HTTP {status}"));
}
let data: Value = resp
.json()
.map_err(|e| format!("npm_info: parse failed: {e}"))?;
let latest = data
.pointer("/dist-tags/latest")
.and_then(Value::as_str)
.unwrap_or("");
let description = data
.get("description")
.and_then(Value::as_str)
.unwrap_or("");
let homepage = data.get("homepage").and_then(Value::as_str).unwrap_or("");
let repo_url = data
.pointer("/repository/url")
.and_then(Value::as_str)
.unwrap_or("");
let license = data.get("license").and_then(Value::as_str).unwrap_or("");
let downloads = client
.get(format!(
"https://api.npmjs.org/downloads/point/last-week/{name}"
))
.send()
.ok()
.and_then(|r| r.json::<Value>().ok())
.and_then(|v| v.get("downloads").and_then(Value::as_u64))
.unwrap_or(0);
Ok(json!({
"name": name,
"description": description,
"latest_version": latest,
"homepage": homepage,
"repository": repo_url,
"license": license,
"weekly_downloads": downloads,
})
.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crate_info_rejects_missing_name() {
let err = run_crate_info("{}").unwrap_err();
assert!(err.contains("missing"), "got: {err}");
}
#[test]
fn npm_info_rejects_missing_name() {
let err = run_npm_info("{}").unwrap_err();
assert!(err.contains("missing"), "got: {err}");
}
#[test]
fn schemas_lists_two_tools() {
let schemas = schemas();
assert_eq!(schemas.len(), 2);
let names: Vec<&str> = schemas
.iter()
.filter_map(|v| v.pointer("/function/name").and_then(Value::as_str))
.collect();
assert_eq!(names, ["crate_info", "npm_info"]);
}
}