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
48pub fn emit_error(localized_msg: &str) {
54 eprintln!("{}: {}", crate::i18n::prefixo_erro(), localized_msg);
55}
56
57pub fn emit_error_i18n(en: &str, pt: &str) {
60 use crate::i18n::{current, Language};
61 let msg = match current() {
62 Language::English => en,
63 Language::Portugues => pt,
64 };
65 emit_error(msg);
66}
67
68#[derive(Serialize)]
107pub struct RememberResponse {
108 pub memory_id: i64,
109 pub name: String,
110 pub namespace: String,
111 pub action: String,
112 pub operation: String,
114 pub version: i64,
115 pub entities_persisted: usize,
116 pub relationships_persisted: usize,
117 pub relationships_truncated: bool,
120 pub chunks_created: usize,
126 pub chunks_persisted: usize,
132 #[serde(default)]
135 pub urls_persisted: usize,
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub extraction_method: Option<String>,
139 pub merged_into_memory_id: Option<i64>,
140 pub warnings: Vec<String>,
141 pub created_at: i64,
143 pub created_at_iso: String,
145 pub elapsed_ms: u64,
147}
148
149#[derive(Serialize, Clone)]
179pub struct RecallItem {
180 pub memory_id: i64,
181 pub name: String,
182 pub namespace: String,
183 #[serde(rename = "type")]
184 pub memory_type: String,
185 pub description: String,
186 pub snippet: String,
187 pub distance: f32,
188 pub source: String,
189 #[serde(skip_serializing_if = "Option::is_none")]
197 pub graph_depth: Option<u32>,
198}
199
200#[derive(Serialize)]
201pub struct RecallResponse {
202 pub query: String,
203 pub k: usize,
204 pub direct_matches: Vec<RecallItem>,
205 pub graph_matches: Vec<RecallItem>,
206 pub results: Vec<RecallItem>,
208 pub elapsed_ms: u64,
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use serde::Serialize;
216
217 #[derive(Serialize)]
218 struct Dummy {
219 val: u32,
220 }
221
222 struct NotSerializable;
224 impl Serialize for NotSerializable {
225 fn serialize<S: serde::Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
226 Err(serde::ser::Error::custom(
227 "falha intencional de serialização",
228 ))
229 }
230 }
231
232 #[test]
233 fn emit_json_retorna_ok_para_valor_valido() {
234 let v = Dummy { val: 42 };
235 assert!(emit_json(&v).is_ok());
236 }
237
238 #[test]
239 fn emit_json_retorna_erro_para_valor_nao_serializavel() {
240 let v = NotSerializable;
241 assert!(emit_json(&v).is_err());
242 }
243
244 #[test]
245 fn emit_json_compact_retorna_ok_para_valor_valido() {
246 let v = Dummy { val: 7 };
247 assert!(emit_json_compact(&v).is_ok());
248 }
249
250 #[test]
251 fn emit_json_compact_retorna_erro_para_valor_nao_serializavel() {
252 let v = NotSerializable;
253 assert!(emit_json_compact(&v).is_err());
254 }
255
256 #[test]
257 fn emit_text_nao_entra_em_panico() {
258 emit_text("mensagem de teste");
259 }
260
261 #[test]
262 fn emit_progress_nao_entra_em_panico() {
263 emit_progress("progresso de teste");
264 }
265
266 #[test]
267 fn remember_response_serializa_corretamente() {
268 let r = RememberResponse {
269 memory_id: 1,
270 name: "teste".to_string(),
271 namespace: "ns".to_string(),
272 action: "created".to_string(),
273 operation: "created".to_string(),
274 version: 1,
275 entities_persisted: 2,
276 relationships_persisted: 3,
277 relationships_truncated: false,
278 chunks_created: 4,
279 chunks_persisted: 4,
280 urls_persisted: 2,
281 extraction_method: None,
282 merged_into_memory_id: None,
283 warnings: vec!["aviso".to_string()],
284 created_at: 1776569715,
285 created_at_iso: "2026-04-19T03:34:15Z".to_string(),
286 elapsed_ms: 123,
287 };
288 let json = serde_json::to_string(&r).unwrap();
289 assert!(json.contains("memory_id"));
290 assert!(json.contains("aviso"));
291 assert!(json.contains("\"namespace\""));
292 assert!(json.contains("\"merged_into_memory_id\""));
293 assert!(json.contains("\"operation\""));
294 assert!(json.contains("\"created_at\""));
295 assert!(json.contains("\"created_at_iso\""));
296 assert!(json.contains("\"elapsed_ms\""));
297 assert!(json.contains("\"urls_persisted\""));
298 assert!(json.contains("\"relationships_truncated\":false"));
299 }
300
301 #[test]
302 fn recall_item_serializa_campo_type_renomeado() {
303 let item = RecallItem {
304 memory_id: 10,
305 name: "entidade".to_string(),
306 namespace: "ns".to_string(),
307 memory_type: "entity".to_string(),
308 description: "desc".to_string(),
309 snippet: "trecho".to_string(),
310 distance: 0.5,
311 source: "db".to_string(),
312 graph_depth: None,
313 };
314 let json = serde_json::to_string(&item).unwrap();
315 assert!(json.contains("\"type\""));
316 assert!(!json.contains("memory_type"));
317 assert!(!json.contains("graph_depth"));
319 }
320
321 #[test]
322 fn recall_response_serializa_com_listas() {
323 let resp = RecallResponse {
324 query: "busca".to_string(),
325 k: 10,
326 direct_matches: vec![],
327 graph_matches: vec![],
328 results: vec![],
329 elapsed_ms: 42,
330 };
331 let json = serde_json::to_string(&resp).unwrap();
332 assert!(json.contains("direct_matches"));
333 assert!(json.contains("graph_matches"));
334 assert!(json.contains("\"k\":"));
335 assert!(json.contains("\"results\""));
336 assert!(json.contains("\"elapsed_ms\""));
337 }
338
339 #[test]
340 fn output_format_default_eh_json() {
341 let fmt = OutputFormat::default();
342 assert!(matches!(fmt, OutputFormat::Json));
343 }
344
345 #[test]
346 fn output_format_variantes_existem() {
347 let _text = OutputFormat::Text;
348 let _md = OutputFormat::Markdown;
349 let _json = OutputFormat::Json;
350 }
351
352 #[test]
353 fn recall_item_clone_produz_valor_igual() {
354 let item = RecallItem {
355 memory_id: 99,
356 name: "clone".to_string(),
357 namespace: "ns".to_string(),
358 memory_type: "relation".to_string(),
359 description: "d".to_string(),
360 snippet: "s".to_string(),
361 distance: 0.1,
362 source: "src".to_string(),
363 graph_depth: Some(2),
364 };
365 let cloned = item.clone();
366 assert_eq!(cloned.memory_id, item.memory_id);
367 assert_eq!(cloned.name, item.name);
368 assert_eq!(cloned.graph_depth, Some(2));
369 }
370}