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)]
87pub struct RememberResponse {
88 pub memory_id: i64,
89 pub name: String,
90 pub namespace: String,
91 pub action: String,
92 pub operation: String,
94 pub version: i64,
95 pub entities_persisted: usize,
96 pub relationships_persisted: usize,
97 pub relationships_truncated: bool,
100 pub chunks_created: usize,
106 pub chunks_persisted: usize,
112 #[serde(default)]
115 pub urls_persisted: usize,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub extraction_method: Option<String>,
119 pub merged_into_memory_id: Option<i64>,
120 pub warnings: Vec<String>,
121 pub created_at: i64,
123 pub created_at_iso: String,
125 pub elapsed_ms: u64,
127}
128
129#[derive(Serialize, Clone)]
159pub struct RecallItem {
160 pub memory_id: i64,
161 pub name: String,
162 pub namespace: String,
163 #[serde(rename = "type")]
164 pub memory_type: String,
165 pub description: String,
166 pub snippet: String,
167 pub distance: f32,
168 pub source: String,
169 #[serde(skip_serializing_if = "Option::is_none")]
177 pub graph_depth: Option<u32>,
178}
179
180#[derive(Serialize)]
181pub struct RecallResponse {
182 pub query: String,
183 pub k: usize,
184 pub direct_matches: Vec<RecallItem>,
185 pub graph_matches: Vec<RecallItem>,
186 pub results: Vec<RecallItem>,
188 pub elapsed_ms: u64,
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use serde::Serialize;
196
197 #[derive(Serialize)]
198 struct Dummy {
199 val: u32,
200 }
201
202 struct NotSerializable;
204 impl Serialize for NotSerializable {
205 fn serialize<S: serde::Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
206 Err(serde::ser::Error::custom(
207 "falha intencional de serialização",
208 ))
209 }
210 }
211
212 #[test]
213 fn emit_json_retorna_ok_para_valor_valido() {
214 let v = Dummy { val: 42 };
215 assert!(emit_json(&v).is_ok());
216 }
217
218 #[test]
219 fn emit_json_retorna_erro_para_valor_nao_serializavel() {
220 let v = NotSerializable;
221 assert!(emit_json(&v).is_err());
222 }
223
224 #[test]
225 fn emit_json_compact_retorna_ok_para_valor_valido() {
226 let v = Dummy { val: 7 };
227 assert!(emit_json_compact(&v).is_ok());
228 }
229
230 #[test]
231 fn emit_json_compact_retorna_erro_para_valor_nao_serializavel() {
232 let v = NotSerializable;
233 assert!(emit_json_compact(&v).is_err());
234 }
235
236 #[test]
237 fn emit_text_nao_entra_em_panico() {
238 emit_text("mensagem de teste");
239 }
240
241 #[test]
242 fn emit_progress_nao_entra_em_panico() {
243 emit_progress("progresso de teste");
244 }
245
246 #[test]
247 fn remember_response_serializa_corretamente() {
248 let r = RememberResponse {
249 memory_id: 1,
250 name: "teste".to_string(),
251 namespace: "ns".to_string(),
252 action: "created".to_string(),
253 operation: "created".to_string(),
254 version: 1,
255 entities_persisted: 2,
256 relationships_persisted: 3,
257 relationships_truncated: false,
258 chunks_created: 4,
259 chunks_persisted: 4,
260 urls_persisted: 2,
261 extraction_method: None,
262 merged_into_memory_id: None,
263 warnings: vec!["aviso".to_string()],
264 created_at: 1776569715,
265 created_at_iso: "2026-04-19T03:34:15Z".to_string(),
266 elapsed_ms: 123,
267 };
268 let json = serde_json::to_string(&r).unwrap();
269 assert!(json.contains("memory_id"));
270 assert!(json.contains("aviso"));
271 assert!(json.contains("\"namespace\""));
272 assert!(json.contains("\"merged_into_memory_id\""));
273 assert!(json.contains("\"operation\""));
274 assert!(json.contains("\"created_at\""));
275 assert!(json.contains("\"created_at_iso\""));
276 assert!(json.contains("\"elapsed_ms\""));
277 assert!(json.contains("\"urls_persisted\""));
278 assert!(json.contains("\"relationships_truncated\":false"));
279 }
280
281 #[test]
282 fn recall_item_serializa_campo_type_renomeado() {
283 let item = RecallItem {
284 memory_id: 10,
285 name: "entidade".to_string(),
286 namespace: "ns".to_string(),
287 memory_type: "entity".to_string(),
288 description: "desc".to_string(),
289 snippet: "trecho".to_string(),
290 distance: 0.5,
291 source: "db".to_string(),
292 graph_depth: None,
293 };
294 let json = serde_json::to_string(&item).unwrap();
295 assert!(json.contains("\"type\""));
296 assert!(!json.contains("memory_type"));
297 assert!(!json.contains("graph_depth"));
299 }
300
301 #[test]
302 fn recall_response_serializa_com_listas() {
303 let resp = RecallResponse {
304 query: "busca".to_string(),
305 k: 10,
306 direct_matches: vec![],
307 graph_matches: vec![],
308 results: vec![],
309 elapsed_ms: 42,
310 };
311 let json = serde_json::to_string(&resp).unwrap();
312 assert!(json.contains("direct_matches"));
313 assert!(json.contains("graph_matches"));
314 assert!(json.contains("\"k\":"));
315 assert!(json.contains("\"results\""));
316 assert!(json.contains("\"elapsed_ms\""));
317 }
318
319 #[test]
320 fn output_format_default_eh_json() {
321 let fmt = OutputFormat::default();
322 assert!(matches!(fmt, OutputFormat::Json));
323 }
324
325 #[test]
326 fn output_format_variantes_existem() {
327 let _text = OutputFormat::Text;
328 let _md = OutputFormat::Markdown;
329 let _json = OutputFormat::Json;
330 }
331
332 #[test]
333 fn recall_item_clone_produz_valor_igual() {
334 let item = RecallItem {
335 memory_id: 99,
336 name: "clone".to_string(),
337 namespace: "ns".to_string(),
338 memory_type: "relation".to_string(),
339 description: "d".to_string(),
340 snippet: "s".to_string(),
341 distance: 0.1,
342 source: "src".to_string(),
343 graph_depth: Some(2),
344 };
345 let cloned = item.clone();
346 assert_eq!(cloned.memory_id, item.memory_id);
347 assert_eq!(cloned.name, item.name);
348 assert_eq!(cloned.graph_depth, Some(2));
349 }
350}