use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::search::{SearchQuery, SymbolQuery};
pub const SOCKET_NAME: &str = "greplmd.sock";
pub fn global_socket_path() -> PathBuf {
let base = std::env::var_os("XDG_RUNTIME_DIR")
.map(PathBuf::from)
.or_else(|| std::env::var_os("TMPDIR").map(PathBuf::from))
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache")))
.unwrap_or_else(std::env::temp_dir);
base.join("greplm").join(SOCKET_NAME)
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Freshness {
#[default]
Lazy,
Flag,
Strict,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoutedRequest {
pub root: PathBuf,
#[serde(default)]
pub freshness: Freshness,
pub req: Request,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalRequest {
#[serde(default)]
pub freshness: Freshness,
#[serde(flatten)]
pub req: Request,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum Request {
Ping,
Status,
Summary,
Reindex {
force: bool,
},
Search(SearchQuery),
Symbols(SymbolQuery),
Refs {
name: String,
limit: usize,
offset: usize,
},
RefsResolved {
name: String,
limit: usize,
offset: usize,
},
Callers {
name: String,
limit: usize,
offset: usize,
},
Callees {
name: String,
limit: usize,
offset: usize,
},
BlastRadius {
name: String,
depth: u32,
limit: usize,
},
Definition {
file: String,
line: u32,
col: u32,
},
ReferencesAt {
file: String,
line: u32,
col: u32,
},
Structural {
pattern: String,
lang: String,
limit: usize,
offset: usize,
},
ContextPack {
task: String,
budget: u64,
},
Blame {
file: String,
line: u32,
},
History {
name: String,
limit: usize,
},
ChangedSince {
rev: String,
},
Outline {
file: String,
},
Snippet {
file: String,
start: u32,
end: u32,
context: u32,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Response {
pub ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Box<serde_json::value::RawValue>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "is_false")]
pub stale: bool,
}
fn is_false(b: &bool) -> bool {
!*b
}
impl Response {
pub fn json<T: serde::Serialize>(value: &T) -> Self {
match serde_json::value::to_raw_value(value) {
Ok(raw) => Response {
ok: true,
result: Some(raw),
error: None,
stale: false,
},
Err(e) => Response::err(e.to_string()),
}
}
pub fn err(message: impl Into<String>) -> Self {
Response {
ok: false,
result: None,
error: Some(message.into()),
stale: false,
}
}
pub fn with_stale(mut self, stale: bool) -> Self {
self.stale = stale;
self
}
pub fn result_text(&self) -> &str {
self.result.as_deref().map(|r| r.get()).unwrap_or("null")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::search::SearchQuery;
#[test]
fn local_request_roundtrips_and_accepts_bare_request() {
let env = LocalRequest {
freshness: Freshness::Strict,
req: Request::Search(SearchQuery {
pattern: "needle".into(),
..Default::default()
}),
};
let json = serde_json::to_string(&env).unwrap();
assert!(json.contains("\"op\":\"search\""), "flattened op: {json}");
assert!(
json.contains("\"freshness\":\"strict\""),
"freshness: {json}"
);
let back: LocalRequest = serde_json::from_str(&json).unwrap();
assert_eq!(back.freshness, Freshness::Strict);
assert!(matches!(back.req, Request::Search(_)));
let bare = serde_json::to_string(&Request::Status).unwrap();
let env: LocalRequest = serde_json::from_str(&bare).unwrap();
assert_eq!(env.freshness, Freshness::Lazy);
assert!(matches!(env.req, Request::Status));
}
#[test]
fn response_stale_is_omitted_when_false() {
let clean = serde_json::to_string(&Response::json(&42)).unwrap();
assert!(!clean.contains("stale"), "clean response: {clean}");
let flagged = serde_json::to_string(&Response::json(&42).with_stale(true)).unwrap();
assert!(flagged.contains("\"stale\":true"), "flagged: {flagged}");
}
}