subcog/io/formats/
yaml.rs1use crate::io::traits::{ExportSink, ExportableMemory, ImportSource, ImportedMemory};
6use crate::{Error, Result};
7use std::io::{BufRead, Write};
8
9pub struct YamlImportSource {
14 memories: Vec<ImportedMemory>,
16 index: usize,
18}
19
20impl YamlImportSource {
21 pub fn new<R: BufRead>(mut reader: R) -> Result<Self> {
29 let mut content = String::new();
30 reader
31 .read_to_string(&mut content)
32 .map_err(|e| Error::OperationFailed {
33 operation: "read_yaml".to_string(),
34 cause: e.to_string(),
35 })?;
36
37 if content.trim().is_empty() {
38 return Ok(Self {
39 memories: Vec::new(),
40 index: 0,
41 });
42 }
43
44 if let Ok(memories) = serde_yaml_ng::from_str::<Vec<ImportedMemory>>(&content) {
46 return Ok(Self { memories, index: 0 });
47 }
48
49 let mut memories = Vec::new();
51 for (doc_index, document) in serde_yaml_ng::Deserializer::from_str(&content).enumerate() {
52 let memory: ImportedMemory =
53 serde::Deserialize::deserialize(document).map_err(|e| {
54 Error::InvalidInput(format!(
55 "Document {}: Failed to parse YAML: {e}",
56 doc_index + 1
57 ))
58 })?;
59 memories.push(memory);
60 }
61
62 Ok(Self { memories, index: 0 })
63 }
64}
65
66impl ImportSource for YamlImportSource {
67 fn next(&mut self) -> Result<Option<ImportedMemory>> {
68 if self.index < self.memories.len() {
69 let memory = self.memories[self.index].clone();
70 self.index += 1;
71 Ok(Some(memory))
72 } else {
73 Ok(None)
74 }
75 }
76
77 fn size_hint(&self) -> Option<usize> {
78 Some(self.memories.len())
79 }
80}
81
82pub struct YamlExportSink<W: Write> {
86 writer: W,
87 count: usize,
89}
90
91impl<W: Write> YamlExportSink<W> {
92 #[must_use]
94 pub const fn new(writer: W) -> Self {
95 Self { writer, count: 0 }
96 }
97}
98
99impl<W: Write + Send> ExportSink for YamlExportSink<W> {
100 fn write(&mut self, memory: &ExportableMemory) -> Result<()> {
101 if self.count > 0 {
103 writeln!(self.writer, "---").map_err(|e| Error::OperationFailed {
104 operation: "write_yaml".to_string(),
105 cause: e.to_string(),
106 })?;
107 }
108
109 serde_yaml_ng::to_writer(&mut self.writer, memory).map_err(|e| Error::OperationFailed {
110 operation: "write_yaml".to_string(),
111 cause: e.to_string(),
112 })?;
113 self.count += 1;
114 Ok(())
115 }
116
117 fn finalize(mut self: Box<Self>) -> Result<()> {
118 self.writer.flush().map_err(|e| Error::OperationFailed {
119 operation: "flush_yaml".to_string(),
120 cause: e.to_string(),
121 })?;
122 Ok(())
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use std::io::Cursor;
130
131 #[test]
132 fn test_import_single_document() {
133 let input = r"
134content: Test memory
135namespace: decisions
136tags:
137 - rust
138 - test
139";
140 let mut source = YamlImportSource::new(Cursor::new(input)).unwrap();
141
142 let memory = source.next().unwrap().unwrap();
143 assert_eq!(memory.content, "Test memory");
144 assert_eq!(memory.namespace, Some("decisions".to_string()));
145 assert_eq!(memory.tags, vec!["rust", "test"]);
146
147 assert!(source.next().unwrap().is_none());
148 }
149
150 #[test]
151 fn test_import_multi_document() {
152 let input = r"---
153content: First memory
154---
155content: Second memory
156namespace: learnings
157";
158 let mut source = YamlImportSource::new(Cursor::new(input)).unwrap();
159
160 let first = source.next().unwrap().unwrap();
161 assert_eq!(first.content, "First memory");
162
163 let second = source.next().unwrap().unwrap();
164 assert_eq!(second.content, "Second memory");
165 assert_eq!(second.namespace, Some("learnings".to_string()));
166
167 assert!(source.next().unwrap().is_none());
168 }
169
170 #[test]
171 fn test_import_array_format() {
172 let input = r"
173- content: First memory
174- content: Second memory
175 tags:
176 - test
177";
178 let mut source = YamlImportSource::new(Cursor::new(input)).unwrap();
179
180 let first = source.next().unwrap().unwrap();
181 assert_eq!(first.content, "First memory");
182
183 let second = source.next().unwrap().unwrap();
184 assert_eq!(second.content, "Second memory");
185
186 assert!(source.next().unwrap().is_none());
187 }
188
189 #[test]
190 fn test_export_yaml() {
191 let mut output = Vec::new();
192 {
193 let mut sink = YamlExportSink::new(&mut output);
194 sink.write(&ExportableMemory {
195 id: "1".to_string(),
196 content: "Test".to_string(),
197 namespace: "decisions".to_string(),
198 domain: "project".to_string(),
199 project_id: None,
200 branch: None,
201 file_path: None,
202 status: "active".to_string(),
203 created_at: 0,
204 updated_at: 0,
205 tags: vec![],
206 source: None,
207 })
208 .unwrap();
209 sink.write(&ExportableMemory {
210 id: "2".to_string(),
211 content: "Second".to_string(),
212 namespace: "learnings".to_string(),
213 domain: "user".to_string(),
214 project_id: None,
215 branch: None,
216 file_path: None,
217 status: "active".to_string(),
218 created_at: 0,
219 updated_at: 0,
220 tags: vec![],
221 source: None,
222 })
223 .unwrap();
224 Box::new(sink).finalize().unwrap();
225 }
226
227 let output_str = String::from_utf8(output).unwrap();
228 assert!(output_str.contains("content: Test"));
229 assert!(output_str.contains("---"));
230 assert!(output_str.contains("content: Second"));
231 }
232
233 #[test]
234 fn test_empty_input() {
235 let input = "";
236 let mut source = YamlImportSource::new(Cursor::new(input)).unwrap();
237 assert!(source.next().unwrap().is_none());
238 }
239}