use serde_json::Value;
pub(crate) fn resolve_tool_name(v: &Value) -> Option<String> {
v.get("tool_name")
.or_else(|| v.get("toolName"))
.and_then(Value::as_str)
.map(str::to_string)
}
pub(crate) fn resolve_tool_args(v: &Value) -> Option<Value> {
if let Some(obj) = v.get("tool_input").filter(|x| x.is_object()) {
return Some(obj.clone());
}
match v.get("toolArgs") {
Some(obj @ Value::Object(_)) => Some(obj.clone()),
Some(Value::String(s)) => serde_json::from_str::<Value>(s)
.ok()
.filter(Value::is_object),
_ => None,
}
}
pub(crate) fn resolve_command(v: &Value, args: Option<&Value>) -> Option<String> {
args.and_then(|a| a.get("command"))
.and_then(Value::as_str)
.or_else(|| v.get("command").and_then(Value::as_str))
.map(str::to_string)
}
pub(crate) const READ_PATH_FIELDS: &[&str] = &["file_path", "path", "target_file"];
pub(crate) fn resolve_path_field<'a>(
args: Option<&Value>,
candidates: &[&'a str],
) -> Option<(&'a str, String)> {
let obj = args?;
candidates.iter().find_map(|&field| {
obj.get(field)
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
.map(|s| (field, s.to_string()))
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn resolves_snake_case_tool_name() {
let v = json!({ "tool_name": "Bash", "tool_input": { "command": "ls" } });
assert_eq!(resolve_tool_name(&v).as_deref(), Some("Bash"));
}
#[test]
fn resolves_camel_case_tool_name() {
let v = json!({ "toolName": "bash", "toolArgs": "{\"command\":\"ls\"}" });
assert_eq!(resolve_tool_name(&v).as_deref(), Some("bash"));
}
#[test]
fn missing_tool_name_is_none() {
assert_eq!(resolve_tool_name(&json!({ "foo": "bar" })), None);
}
#[test]
fn resolves_claude_tool_input_object() {
let v = json!({ "tool_name": "Bash", "tool_input": { "command": "cat foo" } });
let args = resolve_tool_args(&v).expect("args");
assert_eq!(args.get("command").and_then(Value::as_str), Some("cat foo"));
}
#[test]
fn resolves_copilot_tool_args_json_string() {
let v = json!({ "toolName": "bash", "toolArgs": "{\"command\":\"git status\"}" });
let args = resolve_tool_args(&v).expect("args");
assert_eq!(
args.get("command").and_then(Value::as_str),
Some("git status")
);
}
#[test]
fn resolves_copilot_tool_args_object() {
let v = json!({ "toolName": "bash", "toolArgs": { "command": "echo hello" } });
let args = resolve_tool_args(&v).expect("args");
assert_eq!(
args.get("command").and_then(Value::as_str),
Some("echo hello")
);
}
#[test]
fn invalid_tool_args_string_is_none() {
let v = json!({ "toolName": "bash", "toolArgs": "not-json" });
assert!(resolve_tool_args(&v).is_none());
}
#[test]
fn resolve_command_prefers_args_then_top_level() {
let v = json!({ "tool_name": "Bash", "tool_input": { "command": "ls -la" } });
let args = resolve_tool_args(&v);
assert_eq!(
resolve_command(&v, args.as_ref()).as_deref(),
Some("ls -la")
);
let v2 = json!({ "toolName": "bash", "command": "pwd" });
assert_eq!(resolve_command(&v2, None).as_deref(), Some("pwd"));
}
#[test]
fn resolve_path_field_reads_cursor_file_path() {
let args = json!({ "file_path": "/repo/src/main.rs" });
assert_eq!(
resolve_path_field(Some(&args), READ_PATH_FIELDS),
Some(("file_path", "/repo/src/main.rs".to_string()))
);
}
#[test]
fn resolve_path_field_reads_legacy_path() {
let args = json!({ "path": "src/lib.rs" });
assert_eq!(
resolve_path_field(Some(&args), READ_PATH_FIELDS),
Some(("path", "src/lib.rs".to_string()))
);
}
#[test]
fn resolve_path_field_reads_target_file() {
let args = json!({ "target_file": "Cargo.toml" });
assert_eq!(
resolve_path_field(Some(&args), READ_PATH_FIELDS),
Some(("target_file", "Cargo.toml".to_string()))
);
}
#[test]
fn resolve_path_field_prefers_file_path_over_path() {
let args = json!({ "path": "/legacy", "file_path": "/cursor" });
assert_eq!(
resolve_path_field(Some(&args), READ_PATH_FIELDS),
Some(("file_path", "/cursor".to_string()))
);
}
#[test]
fn resolve_path_field_skips_empty_and_missing() {
assert_eq!(resolve_path_field(None, READ_PATH_FIELDS), None);
assert_eq!(
resolve_path_field(Some(&json!({ "other": "x" })), READ_PATH_FIELDS),
None
);
assert_eq!(
resolve_path_field(Some(&json!({ "file_path": "" })), READ_PATH_FIELDS),
None
);
}
}