1use crate::errors::AppError;
2use serde::Serialize;
3
4#[derive(Debug, Clone, Copy, clap::ValueEnum, Default)]
5pub enum OutputFormat {
6 #[default]
7 Json,
8 Text,
9 Markdown,
10}
11
12#[derive(Debug, Clone, Copy, clap::ValueEnum, Default)]
13pub enum JsonOutputFormat {
14 #[default]
15 Json,
16}
17
18pub fn emit_json<T: Serialize>(value: &T) -> Result<(), AppError> {
19 let json = serde_json::to_string_pretty(value)?;
20 println!("{json}");
21 Ok(())
22}
23
24pub fn emit_json_compact<T: Serialize>(value: &T) -> Result<(), AppError> {
25 let json = serde_json::to_string(value)?;
26 println!("{json}");
27 Ok(())
28}
29
30pub fn emit_text(msg: &str) {
31 println!("{msg}");
32}
33
34pub fn emit_progress(msg: &str) {
35 tracing::info!(message = msg);
36}
37
38pub fn emit_progress_i18n(en: &str, pt: &str) {
41 use crate::i18n::{current, Language};
42 match current() {
43 Language::English => tracing::info!(message = en),
44 Language::Portugues => tracing::info!(message = pt),
45 }
46}
47
48#[derive(Serialize)]
82pub struct RememberResponse {
83 pub memory_id: i64,
84 pub name: String,
85 pub namespace: String,
86 pub action: String,
87 pub operation: String,
89 pub version: i64,
90 pub entities_persisted: usize,
91 pub relationships_persisted: usize,
92 pub chunks_created: usize,
98 pub chunks_persisted: usize,
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub extraction_method: Option<String>,
107 pub merged_into_memory_id: Option<i64>,
108 pub warnings: Vec<String>,
109 pub created_at: i64,
111 pub created_at_iso: String,
113 pub elapsed_ms: u64,
115}
116
117#[derive(Serialize, Clone)]
147pub struct RecallItem {
148 pub memory_id: i64,
149 pub name: String,
150 pub namespace: String,
151 #[serde(rename = "type")]
152 pub memory_type: String,
153 pub description: String,
154 pub snippet: String,
155 pub distance: f32,
156 pub source: String,
157 #[serde(skip_serializing_if = "Option::is_none")]
165 pub graph_depth: Option<u32>,
166}
167
168#[derive(Serialize)]
169pub struct RecallResponse {
170 pub query: String,
171 pub k: usize,
172 pub direct_matches: Vec<RecallItem>,
173 pub graph_matches: Vec<RecallItem>,
174 pub results: Vec<RecallItem>,
176 pub elapsed_ms: u64,
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use serde::Serialize;
184
185 #[derive(Serialize)]
186 struct Dummy {
187 val: u32,
188 }
189
190 struct NotSerializable;
192 impl Serialize for NotSerializable {
193 fn serialize<S: serde::Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
194 Err(serde::ser::Error::custom(
195 "falha intencional de serialização",
196 ))
197 }
198 }
199
200 #[test]
201 fn emit_json_retorna_ok_para_valor_valido() {
202 let v = Dummy { val: 42 };
203 assert!(emit_json(&v).is_ok());
204 }
205
206 #[test]
207 fn emit_json_retorna_erro_para_valor_nao_serializavel() {
208 let v = NotSerializable;
209 assert!(emit_json(&v).is_err());
210 }
211
212 #[test]
213 fn emit_json_compact_retorna_ok_para_valor_valido() {
214 let v = Dummy { val: 7 };
215 assert!(emit_json_compact(&v).is_ok());
216 }
217
218 #[test]
219 fn emit_json_compact_retorna_erro_para_valor_nao_serializavel() {
220 let v = NotSerializable;
221 assert!(emit_json_compact(&v).is_err());
222 }
223
224 #[test]
225 fn emit_text_nao_entra_em_panico() {
226 emit_text("mensagem de teste");
227 }
228
229 #[test]
230 fn emit_progress_nao_entra_em_panico() {
231 emit_progress("progresso de teste");
232 }
233
234 #[test]
235 fn remember_response_serializa_corretamente() {
236 let r = RememberResponse {
237 memory_id: 1,
238 name: "teste".to_string(),
239 namespace: "ns".to_string(),
240 action: "created".to_string(),
241 operation: "created".to_string(),
242 version: 1,
243 entities_persisted: 2,
244 relationships_persisted: 3,
245 chunks_created: 4,
246 chunks_persisted: 4,
247 extraction_method: None,
248 merged_into_memory_id: None,
249 warnings: vec!["aviso".to_string()],
250 created_at: 1776569715,
251 created_at_iso: "2026-04-19T03:34:15Z".to_string(),
252 elapsed_ms: 123,
253 };
254 let json = serde_json::to_string(&r).unwrap();
255 assert!(json.contains("memory_id"));
256 assert!(json.contains("aviso"));
257 assert!(json.contains("\"namespace\""));
258 assert!(json.contains("\"merged_into_memory_id\""));
259 assert!(json.contains("\"operation\""));
260 assert!(json.contains("\"created_at\""));
261 assert!(json.contains("\"created_at_iso\""));
262 assert!(json.contains("\"elapsed_ms\""));
263 }
264
265 #[test]
266 fn recall_item_serializa_campo_type_renomeado() {
267 let item = RecallItem {
268 memory_id: 10,
269 name: "entidade".to_string(),
270 namespace: "ns".to_string(),
271 memory_type: "entity".to_string(),
272 description: "desc".to_string(),
273 snippet: "trecho".to_string(),
274 distance: 0.5,
275 source: "db".to_string(),
276 graph_depth: None,
277 };
278 let json = serde_json::to_string(&item).unwrap();
279 assert!(json.contains("\"type\""));
280 assert!(!json.contains("memory_type"));
281 assert!(!json.contains("graph_depth"));
283 }
284
285 #[test]
286 fn recall_response_serializa_com_listas() {
287 let resp = RecallResponse {
288 query: "busca".to_string(),
289 k: 10,
290 direct_matches: vec![],
291 graph_matches: vec![],
292 results: vec![],
293 elapsed_ms: 42,
294 };
295 let json = serde_json::to_string(&resp).unwrap();
296 assert!(json.contains("direct_matches"));
297 assert!(json.contains("graph_matches"));
298 assert!(json.contains("\"k\":"));
299 assert!(json.contains("\"results\""));
300 assert!(json.contains("\"elapsed_ms\""));
301 }
302
303 #[test]
304 fn output_format_default_eh_json() {
305 let fmt = OutputFormat::default();
306 assert!(matches!(fmt, OutputFormat::Json));
307 }
308
309 #[test]
310 fn output_format_variantes_existem() {
311 let _text = OutputFormat::Text;
312 let _md = OutputFormat::Markdown;
313 let _json = OutputFormat::Json;
314 }
315
316 #[test]
317 fn recall_item_clone_produz_valor_igual() {
318 let item = RecallItem {
319 memory_id: 99,
320 name: "clone".to_string(),
321 namespace: "ns".to_string(),
322 memory_type: "relation".to_string(),
323 description: "d".to_string(),
324 snippet: "s".to_string(),
325 distance: 0.1,
326 source: "src".to_string(),
327 graph_depth: Some(2),
328 };
329 let cloned = item.clone();
330 assert_eq!(cloned.memory_id, item.memory_id);
331 assert_eq!(cloned.name, item.name);
332 assert_eq!(cloned.graph_depth, Some(2));
333 }
334}