use std::path::Path;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const WIRE_VERSION: u32 = 2;
const HELLO_KEY: &str = "leindex_hello";
const ACK_KEY: &str = "leindex_ack";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Hello {
pub wire: u32,
pub version: String,
pub cwd: Option<String>,
pub pid: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Ack {
pub wire: u32,
pub version: String,
pub pid: u32,
pub started_ms: u64,
pub clients: usize,
pub ok: bool,
pub error: Option<String>,
}
pub fn hello_line(hello: &Hello) -> String {
format!("{}\n", serde_json::json!({ HELLO_KEY: hello }))
}
pub fn ack_line(ack: &Ack) -> String {
format!("{}\n", serde_json::json!({ ACK_KEY: ack }))
}
pub fn parse_hello(line: &str) -> Option<Hello> {
let value: Value = serde_json::from_str(line.trim()).ok()?;
serde_json::from_value(value.get(HELLO_KEY)?.clone()).ok()
}
pub fn parse_ack(line: &str) -> Option<Ack> {
let value: Value = serde_json::from_str(line.trim()).ok()?;
serde_json::from_value(value.get(ACK_KEY)?.clone()).ok()
}
pub fn is_projectish_cwd(cwd: &Path) -> bool {
if cwd.parent().is_none() {
return false;
}
let home = dirs::home_dir();
home.as_deref() != Some(cwd)
}
pub fn with_default_project(payload: &str, cwd: &str) -> Option<String> {
if !payload.contains("tools/call") {
return None;
}
let mut value: Value = serde_json::from_str(payload).ok()?;
if value.get("method")?.as_str()? != "tools/call" {
return None;
}
let params = value.get_mut("params")?.as_object_mut()?;
let arguments = params
.entry("arguments")
.or_insert_with(|| Value::Object(Default::default()));
if arguments.is_null() {
*arguments = Value::Object(Default::default());
}
let arguments = arguments.as_object_mut()?;
if arguments
.get("project_path")
.is_some_and(|existing| existing.as_str().is_some_and(|s| !s.is_empty()))
{
return None;
}
arguments.insert("project_path".into(), Value::String(cwd.to_string()));
serde_json::to_string(&value).ok()
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_hello_round_trip() {
let hello = Hello {
wire: WIRE_VERSION,
version: "2.0.0".into(),
cwd: Some("/work/p".into()),
pid: 7,
};
let line = hello_line(&hello);
assert!(line.ends_with('\n'));
assert_eq!(parse_hello(&line), Some(hello));
}
#[test]
fn test_ack_round_trip() {
let ack = Ack {
wire: WIRE_VERSION,
version: "2.0.0".into(),
pid: 9,
started_ms: 123,
clients: 2,
ok: true,
error: None,
};
assert_eq!(parse_ack(&ack_line(&ack)), Some(ack));
}
#[test]
fn test_ordinary_frames_are_not_hello() {
assert_eq!(
parse_hello(r#"{"jsonrpc":"2.0","id":1,"method":"initialize"}"#),
None
);
assert_eq!(parse_hello("not json"), None);
assert_eq!(parse_ack(r#"{"jsonrpc":"2.0"}"#), None);
}
#[test]
fn test_default_project_fills_only_missing_project_path() {
let call = r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"leindex_explore","arguments":{"mode":"find","pattern":"x"}}}"#;
let patched = with_default_project(call, "/work/p").expect("patched");
let value: Value = serde_json::from_str(&patched).unwrap();
assert_eq!(value["params"]["arguments"]["project_path"], "/work/p");
assert_eq!(value["params"]["arguments"]["pattern"], "x");
assert_eq!(value["id"], 3);
let explicit = r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"t","arguments":{"project_path":"/other"}}}"#;
assert_eq!(with_default_project(explicit, "/work/p"), None);
}
#[test]
fn test_default_project_creates_missing_arguments_and_ignores_other_methods() {
let call =
r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"leindex_analyze"}}"#;
let patched = with_default_project(call, "/w").expect("patched");
let value: Value = serde_json::from_str(&patched).unwrap();
assert_eq!(value["params"]["arguments"]["project_path"], "/w");
let list = r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#;
assert_eq!(with_default_project(list, "/w"), None);
let notification_mentioning_it =
r#"{"jsonrpc":"2.0","method":"notifications/x","params":{"note":"tools/call"}}"#;
assert_eq!(with_default_project(notification_mentioning_it, "/w"), None);
}
#[test]
fn test_root_and_home_are_not_projects() {
assert!(!is_projectish_cwd(Path::new("/")));
if let Some(home) = dirs::home_dir() {
assert!(!is_projectish_cwd(&home));
}
assert!(is_projectish_cwd(Path::new("/work/some/project")));
}
}