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)]
81pub struct RememberResponse {
82 pub memory_id: i64,
83 pub name: String,
84 pub namespace: String,
85 pub action: String,
86 pub operation: String,
88 pub version: i64,
89 pub entities_persisted: usize,
90 pub relationships_persisted: usize,
91 pub chunks_created: usize,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub extraction_method: Option<String>,
95 pub merged_into_memory_id: Option<i64>,
96 pub warnings: Vec<String>,
97 pub created_at: i64,
99 pub created_at_iso: String,
101 pub elapsed_ms: u64,
103}
104
105#[derive(Serialize, Clone)]
134pub struct RecallItem {
135 pub memory_id: i64,
136 pub name: String,
137 pub namespace: String,
138 #[serde(rename = "type")]
139 pub memory_type: String,
140 pub description: String,
141 pub snippet: String,
142 pub distance: f32,
143 pub source: String,
144}
145
146#[derive(Serialize)]
147pub struct RecallResponse {
148 pub query: String,
149 pub k: usize,
150 pub direct_matches: Vec<RecallItem>,
151 pub graph_matches: Vec<RecallItem>,
152 pub results: Vec<RecallItem>,
154 pub elapsed_ms: u64,
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use serde::Serialize;
162
163 #[derive(Serialize)]
164 struct Dummy {
165 val: u32,
166 }
167
168 struct NotSerializable;
170 impl Serialize for NotSerializable {
171 fn serialize<S: serde::Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
172 Err(serde::ser::Error::custom(
173 "falha intencional de serialização",
174 ))
175 }
176 }
177
178 #[test]
179 fn emit_json_retorna_ok_para_valor_valido() {
180 let v = Dummy { val: 42 };
181 assert!(emit_json(&v).is_ok());
182 }
183
184 #[test]
185 fn emit_json_retorna_erro_para_valor_nao_serializavel() {
186 let v = NotSerializable;
187 assert!(emit_json(&v).is_err());
188 }
189
190 #[test]
191 fn emit_json_compact_retorna_ok_para_valor_valido() {
192 let v = Dummy { val: 7 };
193 assert!(emit_json_compact(&v).is_ok());
194 }
195
196 #[test]
197 fn emit_json_compact_retorna_erro_para_valor_nao_serializavel() {
198 let v = NotSerializable;
199 assert!(emit_json_compact(&v).is_err());
200 }
201
202 #[test]
203 fn emit_text_nao_entra_em_panico() {
204 emit_text("mensagem de teste");
205 }
206
207 #[test]
208 fn emit_progress_nao_entra_em_panico() {
209 emit_progress("progresso de teste");
210 }
211
212 #[test]
213 fn remember_response_serializa_corretamente() {
214 let r = RememberResponse {
215 memory_id: 1,
216 name: "teste".to_string(),
217 namespace: "ns".to_string(),
218 action: "created".to_string(),
219 operation: "created".to_string(),
220 version: 1,
221 entities_persisted: 2,
222 relationships_persisted: 3,
223 chunks_created: 4,
224 extraction_method: None,
225 merged_into_memory_id: None,
226 warnings: vec!["aviso".to_string()],
227 created_at: 1776569715,
228 created_at_iso: "2026-04-19T03:34:15Z".to_string(),
229 elapsed_ms: 123,
230 };
231 let json = serde_json::to_string(&r).unwrap();
232 assert!(json.contains("memory_id"));
233 assert!(json.contains("aviso"));
234 assert!(json.contains("\"namespace\""));
235 assert!(json.contains("\"merged_into_memory_id\""));
236 assert!(json.contains("\"operation\""));
237 assert!(json.contains("\"created_at\""));
238 assert!(json.contains("\"created_at_iso\""));
239 assert!(json.contains("\"elapsed_ms\""));
240 }
241
242 #[test]
243 fn recall_item_serializa_campo_type_renomeado() {
244 let item = RecallItem {
245 memory_id: 10,
246 name: "entidade".to_string(),
247 namespace: "ns".to_string(),
248 memory_type: "entity".to_string(),
249 description: "desc".to_string(),
250 snippet: "trecho".to_string(),
251 distance: 0.5,
252 source: "db".to_string(),
253 };
254 let json = serde_json::to_string(&item).unwrap();
255 assert!(json.contains("\"type\""));
256 assert!(!json.contains("memory_type"));
257 }
258
259 #[test]
260 fn recall_response_serializa_com_listas() {
261 let resp = RecallResponse {
262 query: "busca".to_string(),
263 k: 10,
264 direct_matches: vec![],
265 graph_matches: vec![],
266 results: vec![],
267 elapsed_ms: 42,
268 };
269 let json = serde_json::to_string(&resp).unwrap();
270 assert!(json.contains("direct_matches"));
271 assert!(json.contains("graph_matches"));
272 assert!(json.contains("\"k\":"));
273 assert!(json.contains("\"results\""));
274 assert!(json.contains("\"elapsed_ms\""));
275 }
276
277 #[test]
278 fn output_format_default_eh_json() {
279 let fmt = OutputFormat::default();
280 assert!(matches!(fmt, OutputFormat::Json));
281 }
282
283 #[test]
284 fn output_format_variantes_existem() {
285 let _text = OutputFormat::Text;
286 let _md = OutputFormat::Markdown;
287 let _json = OutputFormat::Json;
288 }
289
290 #[test]
291 fn recall_item_clone_produz_valor_igual() {
292 let item = RecallItem {
293 memory_id: 99,
294 name: "clone".to_string(),
295 namespace: "ns".to_string(),
296 memory_type: "relation".to_string(),
297 description: "d".to_string(),
298 snippet: "s".to_string(),
299 distance: 0.1,
300 source: "src".to_string(),
301 };
302 let cloned = item.clone();
303 assert_eq!(cloned.memory_id, item.memory_id);
304 assert_eq!(cloned.name, item.name);
305 }
306}