1use crate::errors::AppError;
7use serde::Serialize;
8
9#[derive(Debug, Clone, Copy, clap::ValueEnum, Default)]
10pub enum OutputFormat {
11 #[default]
12 Json,
13 Text,
14 Markdown,
15}
16
17#[derive(Debug, Clone, Copy, clap::ValueEnum, Default)]
18pub enum JsonOutputFormat {
19 #[default]
20 Json,
21}
22
23pub fn emit_json<T: Serialize>(value: &T) -> Result<(), AppError> {
24 let json = serde_json::to_string_pretty(value)?;
25 println!("{json}");
26 Ok(())
27}
28
29pub fn emit_json_compact<T: Serialize>(value: &T) -> Result<(), AppError> {
30 let json = serde_json::to_string(value)?;
31 println!("{json}");
32 Ok(())
33}
34
35pub fn emit_text(msg: &str) {
36 println!("{msg}");
37}
38
39pub fn emit_progress(msg: &str) {
40 tracing::info!(message = msg);
41}
42
43pub fn emit_progress_i18n(en: &str, pt: &str) {
46 use crate::i18n::{current, Language};
47 match current() {
48 Language::English => tracing::info!(message = en),
49 Language::Portuguese => tracing::info!(message = pt),
50 }
51}
52
53pub fn emit_error(localized_msg: &str) {
59 eprintln!("{}: {}", crate::i18n::error_prefix(), localized_msg);
60}
61
62pub fn emit_error_i18n(en: &str, pt: &str) {
65 use crate::i18n::{current, Language};
66 let msg = match current() {
67 Language::English => en,
68 Language::Portuguese => pt,
69 };
70 emit_error(msg);
71}
72
73#[derive(Serialize)]
112pub struct RememberResponse {
113 pub memory_id: i64,
114 pub name: String,
115 pub namespace: String,
116 pub action: String,
117 pub operation: String,
119 pub version: i64,
120 pub entities_persisted: usize,
121 pub relationships_persisted: usize,
122 pub relationships_truncated: bool,
125 pub chunks_created: usize,
131 pub chunks_persisted: usize,
137 #[serde(default)]
140 pub urls_persisted: usize,
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub extraction_method: Option<String>,
144 pub merged_into_memory_id: Option<i64>,
145 pub warnings: Vec<String>,
146 pub created_at: i64,
148 pub created_at_iso: String,
150 pub elapsed_ms: u64,
152}
153
154#[derive(Serialize, Clone)]
184pub struct RecallItem {
185 pub memory_id: i64,
186 pub name: String,
187 pub namespace: String,
188 #[serde(rename = "type")]
189 pub memory_type: String,
190 pub description: String,
191 pub snippet: String,
192 pub distance: f32,
193 pub source: String,
194 #[serde(skip_serializing_if = "Option::is_none")]
202 pub graph_depth: Option<u32>,
203}
204
205#[derive(Serialize)]
206pub struct RecallResponse {
207 pub query: String,
208 pub k: usize,
209 pub direct_matches: Vec<RecallItem>,
210 pub graph_matches: Vec<RecallItem>,
211 pub results: Vec<RecallItem>,
213 pub elapsed_ms: u64,
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use serde::Serialize;
221
222 #[derive(Serialize)]
223 struct Dummy {
224 val: u32,
225 }
226
227 struct NotSerializable;
229 impl Serialize for NotSerializable {
230 fn serialize<S: serde::Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
231 Err(serde::ser::Error::custom(
232 "falha intencional de serialização",
233 ))
234 }
235 }
236
237 #[test]
238 fn emit_json_retorna_ok_para_valor_valido() {
239 let v = Dummy { val: 42 };
240 assert!(emit_json(&v).is_ok());
241 }
242
243 #[test]
244 fn emit_json_retorna_erro_para_valor_nao_serializavel() {
245 let v = NotSerializable;
246 assert!(emit_json(&v).is_err());
247 }
248
249 #[test]
250 fn emit_json_compact_retorna_ok_para_valor_valido() {
251 let v = Dummy { val: 7 };
252 assert!(emit_json_compact(&v).is_ok());
253 }
254
255 #[test]
256 fn emit_json_compact_retorna_erro_para_valor_nao_serializavel() {
257 let v = NotSerializable;
258 assert!(emit_json_compact(&v).is_err());
259 }
260
261 #[test]
262 fn emit_text_nao_entra_em_panico() {
263 emit_text("mensagem de teste");
264 }
265
266 #[test]
267 fn emit_progress_nao_entra_em_panico() {
268 emit_progress("progresso de teste");
269 }
270
271 #[test]
272 fn remember_response_serializa_corretamente() {
273 let r = RememberResponse {
274 memory_id: 1,
275 name: "teste".to_string(),
276 namespace: "ns".to_string(),
277 action: "created".to_string(),
278 operation: "created".to_string(),
279 version: 1,
280 entities_persisted: 2,
281 relationships_persisted: 3,
282 relationships_truncated: false,
283 chunks_created: 4,
284 chunks_persisted: 4,
285 urls_persisted: 2,
286 extraction_method: None,
287 merged_into_memory_id: None,
288 warnings: vec!["aviso".to_string()],
289 created_at: 1776569715,
290 created_at_iso: "2026-04-19T03:34:15Z".to_string(),
291 elapsed_ms: 123,
292 };
293 let json = serde_json::to_string(&r).unwrap();
294 assert!(json.contains("memory_id"));
295 assert!(json.contains("aviso"));
296 assert!(json.contains("\"namespace\""));
297 assert!(json.contains("\"merged_into_memory_id\""));
298 assert!(json.contains("\"operation\""));
299 assert!(json.contains("\"created_at\""));
300 assert!(json.contains("\"created_at_iso\""));
301 assert!(json.contains("\"elapsed_ms\""));
302 assert!(json.contains("\"urls_persisted\""));
303 assert!(json.contains("\"relationships_truncated\":false"));
304 }
305
306 #[test]
307 fn recall_item_serializa_campo_type_renomeado() {
308 let item = RecallItem {
309 memory_id: 10,
310 name: "entidade".to_string(),
311 namespace: "ns".to_string(),
312 memory_type: "entity".to_string(),
313 description: "desc".to_string(),
314 snippet: "trecho".to_string(),
315 distance: 0.5,
316 source: "db".to_string(),
317 graph_depth: None,
318 };
319 let json = serde_json::to_string(&item).unwrap();
320 assert!(json.contains("\"type\""));
321 assert!(!json.contains("memory_type"));
322 assert!(!json.contains("graph_depth"));
324 }
325
326 #[test]
327 fn recall_response_serializa_com_listas() {
328 let resp = RecallResponse {
329 query: "busca".to_string(),
330 k: 10,
331 direct_matches: vec![],
332 graph_matches: vec![],
333 results: vec![],
334 elapsed_ms: 42,
335 };
336 let json = serde_json::to_string(&resp).unwrap();
337 assert!(json.contains("direct_matches"));
338 assert!(json.contains("graph_matches"));
339 assert!(json.contains("\"k\":"));
340 assert!(json.contains("\"results\""));
341 assert!(json.contains("\"elapsed_ms\""));
342 }
343
344 #[test]
345 fn output_format_default_eh_json() {
346 let fmt = OutputFormat::default();
347 assert!(matches!(fmt, OutputFormat::Json));
348 }
349
350 #[test]
351 fn output_format_variantes_existem() {
352 let _text = OutputFormat::Text;
353 let _md = OutputFormat::Markdown;
354 let _json = OutputFormat::Json;
355 }
356
357 #[test]
358 fn recall_item_clone_produz_valor_igual() {
359 let item = RecallItem {
360 memory_id: 99,
361 name: "clone".to_string(),
362 namespace: "ns".to_string(),
363 memory_type: "relation".to_string(),
364 description: "d".to_string(),
365 snippet: "s".to_string(),
366 distance: 0.1,
367 source: "src".to_string(),
368 graph_depth: Some(2),
369 };
370 let cloned = item.clone();
371 assert_eq!(cloned.memory_id, item.memory_id);
372 assert_eq!(cloned.name, item.name);
373 assert_eq!(cloned.graph_depth, Some(2));
374 }
375}