1use serde::Serialize;
20use time::format_description::well_known::Rfc3339;
21use time::OffsetDateTime;
22
23use crate::db::Symbol;
24use crate::error::Result;
25
26#[derive(Debug, Clone, Serialize)]
31pub struct SymbolRef {
32 pub name: String,
33 pub qualified_name: Option<String>,
34 pub kind: String,
35 pub file: String,
36 pub line_start: i64,
37 pub line_end: i64,
38}
39
40impl From<&Symbol> for SymbolRef {
41 fn from(s: &Symbol) -> Self {
42 SymbolRef {
43 name: s.name.clone(),
44 qualified_name: s.qualified_name.clone(),
45 kind: s.kind.as_str().to_string(),
46 file: s.file_path.clone(),
47 line_start: s.line_start as i64,
48 line_end: s.line_end as i64,
49 }
50 }
51}
52
53impl SymbolRef {
54 pub fn to_value(&self) -> serde_json::Value {
56 serde_json::to_value(self).unwrap_or(serde_json::Value::Null)
57 }
58}
59
60pub fn envelope(command: &str, data: serde_json::Value) -> serde_json::Value {
62 let generated_at = OffsetDateTime::now_utc()
63 .format(&Rfc3339)
64 .unwrap_or_default();
65 serde_json::json!({
66 "ctx_version": env!("CARGO_PKG_VERSION"),
67 "command": command,
68 "generated_at": generated_at,
69 "data": data,
70 })
71}
72
73pub fn emit(command: &str, data: serde_json::Value) -> Result<()> {
77 println!(
78 "{}",
79 serde_json::to_string_pretty(&envelope(command, data))?
80 );
81 Ok(())
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::db::{SymbolKind, Visibility};
88
89 fn sample_symbol() -> Symbol {
90 Symbol {
91 id: "src/main.rs::main".to_string(),
92 file_path: "src/main.rs".to_string(),
93 name: "main".to_string(),
94 qualified_name: Some("crate::main".to_string()),
95 kind: SymbolKind::Function,
96 visibility: Visibility::Public,
97 signature: Some("fn main()".to_string()),
98 brief: None,
99 docstring: None,
100 line_start: 3,
101 line_end: 10,
102 col_start: 0,
103 col_end: 1,
104 parent_id: None,
105 source: None,
106 }
107 }
108
109 #[test]
110 fn test_envelope_shape() {
111 let value = envelope("query.find", serde_json::json!({"symbols": []}));
112 let obj = value.as_object().unwrap();
113
114 assert_eq!(obj.len(), 4);
115 assert_eq!(
116 obj.get("ctx_version").unwrap().as_str().unwrap(),
117 env!("CARGO_PKG_VERSION")
118 );
119 assert_eq!(obj.get("command").unwrap().as_str().unwrap(), "query.find");
120 assert_eq!(value["data"]["symbols"], serde_json::json!([]));
121
122 let ts = obj.get("generated_at").unwrap().as_str().unwrap();
124 let parsed = OffsetDateTime::parse(ts, &Rfc3339);
125 assert!(parsed.is_ok(), "generated_at is not RFC3339: {}", ts);
126 }
127
128 #[test]
129 fn test_symbol_ref_snake_case_serialization() {
130 let symbol = sample_symbol();
131 let value = SymbolRef::from(&symbol).to_value();
132 let obj = value.as_object().unwrap();
133
134 let keys: Vec<&str> = obj.keys().map(|k| k.as_str()).collect();
136 assert_eq!(
137 keys,
138 vec![
139 "file",
140 "kind",
141 "line_end",
142 "line_start",
143 "name",
144 "qualified_name"
145 ]
146 );
147 assert_eq!(value["name"], "main");
148 assert_eq!(value["qualified_name"], "crate::main");
149 assert_eq!(value["kind"], "function");
150 assert_eq!(value["file"], "src/main.rs");
151 assert_eq!(value["line_start"], 3);
152 assert_eq!(value["line_end"], 10);
153 }
154
155 #[test]
156 fn test_symbol_ref_null_qualified_name() {
157 let mut symbol = sample_symbol();
158 symbol.qualified_name = None;
159 let value = SymbolRef::from(&symbol).to_value();
160 assert!(value["qualified_name"].is_null());
161 }
162
163 #[test]
164 fn test_emitted_output_is_exactly_one_json_document() {
165 let printed = format!(
168 "{}\n",
169 serde_json::to_string_pretty(&envelope(
170 "search",
171 serde_json::json!({"query": "q", "results": []})
172 ))
173 .unwrap()
174 );
175
176 let mut stream =
177 serde_json::Deserializer::from_str(&printed).into_iter::<serde_json::Value>();
178 let first = stream.next().expect("expected one document").unwrap();
179 assert_eq!(first["command"], "search");
180 assert!(stream.next().is_none(), "expected exactly one document");
181 }
182}