1use crate::model::mem_file::MemFile;
4
5#[must_use]
35pub fn render_mem(mem: &MemFile) -> String {
36 let mut out = String::new();
37
38 out.push_str(&format!("# agm.mem: {}\n", mem.format_version));
40 out.push_str(&format!("# package: {}\n", mem.package));
41 out.push_str(&format!("# updated_at: {}\n", mem.updated_at));
42
43 for (key, entry) in &mem.entries {
45 out.push('\n');
46
47 out.push_str(&format!("entry {key}\n"));
48 out.push_str(&format!("topic: {}\n", entry.topic));
49 out.push_str(&format!("scope: {}\n", entry.scope));
50 out.push_str(&format!("ttl: {}\n", entry.ttl));
51
52 let mut value_lines = entry.value.split('\n');
54 if let Some(first) = value_lines.next() {
55 out.push_str(&format!("value: {first}\n"));
56 for cont in value_lines {
57 out.push_str(&format!(" {cont}\n"));
58 }
59 } else {
60 out.push_str("value: \n");
61 }
62
63 out.push_str(&format!("created_at: {}\n", entry.created_at));
64 out.push_str(&format!("updated_at: {}\n", entry.updated_at));
65 }
66
67 out
68}
69
70#[must_use]
76pub fn render_mem_json(mem: &MemFile) -> String {
77 serde_json::to_string_pretty(mem).expect("MemFile serialization cannot fail")
78}
79
80#[must_use]
88pub fn render_mem_sql(mem: &MemFile) -> String {
89 fn escape(s: &str) -> String {
90 s.replace('\'', "''")
91 }
92
93 let mut out = String::new();
94
95 for (key, entry) in &mem.entries {
96 out.push_str(&format!(
97 "INSERT INTO agm_memory (package, entry_key, topic, scope, ttl, value, created_at, updated_at) VALUES ('{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}');\n",
98 escape(&mem.package),
99 escape(key),
100 escape(&entry.topic),
101 escape(&entry.scope.to_string()),
102 escape(&entry.ttl.to_string()),
103 escape(&entry.value),
104 escape(&entry.created_at),
105 escape(&entry.updated_at),
106 ));
107 }
108
109 out
110}
111
112#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::model::mem_file::{MemFile, MemFileEntry};
120 use crate::model::memory::{MemoryScope, MemoryTtl};
121 use std::collections::BTreeMap;
122
123 fn minimal_mem() -> MemFile {
124 MemFile {
125 format_version: "1.0".to_owned(),
126 package: "test.pkg".to_owned(),
127 updated_at: "2026-04-08T10:00:00Z".to_owned(),
128 entries: BTreeMap::new(),
129 }
130 }
131
132 fn make_entry(
133 key: &str,
134 value: &str,
135 ttl: MemoryTtl,
136 scope: MemoryScope,
137 ) -> (String, MemFileEntry) {
138 (
139 key.to_owned(),
140 MemFileEntry {
141 topic: "infrastructure".to_owned(),
142 scope,
143 ttl,
144 value: value.to_owned(),
145 created_at: "2026-04-08T10:00:00Z".to_owned(),
146 updated_at: "2026-04-08T10:00:00Z".to_owned(),
147 },
148 )
149 }
150
151 fn full_mem() -> MemFile {
152 let mut entries = BTreeMap::new();
153 let (k, e) = make_entry(
154 "project.db_version",
155 "PostgreSQL 15.2",
156 MemoryTtl::Permanent,
157 MemoryScope::Project,
158 );
159 entries.insert(k, e);
160 MemFile {
161 format_version: "1.0".to_owned(),
162 package: "acme.migration".to_owned(),
163 updated_at: "2026-04-08T15:30:00Z".to_owned(),
164 entries,
165 }
166 }
167
168 #[test]
173 fn test_render_mem_headers_present() {
174 let output = render_mem(&minimal_mem());
175 assert!(output.contains("# agm.mem: 1.0"));
176 assert!(output.contains("# package: test.pkg"));
177 assert!(output.contains("# updated_at: 2026-04-08T10:00:00Z"));
178 }
179
180 #[test]
185 fn test_render_mem_entry_fields_present() {
186 let output = render_mem(&full_mem());
187 assert!(output.contains("entry project.db_version"));
188 assert!(output.contains("topic: infrastructure"));
189 assert!(output.contains("scope: project"));
190 assert!(output.contains("ttl: permanent"));
191 assert!(output.contains("value: PostgreSQL 15.2"));
192 assert!(output.contains("created_at: 2026-04-08T10:00:00Z"));
193 assert!(output.contains("updated_at: 2026-04-08T10:00:00Z"));
194 }
195
196 #[test]
201 fn test_render_mem_multiline_value_indented() {
202 let mut entries = BTreeMap::new();
203 let (k, e) = make_entry(
204 "ml.entry",
205 "line one\nline two\nline three",
206 MemoryTtl::Permanent,
207 MemoryScope::Project,
208 );
209 entries.insert(k, e);
210 let mem = MemFile {
211 format_version: "1.0".to_owned(),
212 package: "test.pkg".to_owned(),
213 updated_at: "2026-04-08T10:00:00Z".to_owned(),
214 entries,
215 };
216 let output = render_mem(&mem);
217 assert!(output.contains("value: line one\n line two\n line three\n"));
218 }
219
220 #[test]
225 fn test_render_mem_roundtrip_single_line_value() {
226 use crate::parser::mem::parse_mem;
227
228 let mem = full_mem();
229 let rendered = render_mem(&mem);
230 let parsed = parse_mem(&rendered).expect("roundtrip parse failed");
231 assert_eq!(mem, parsed);
232 }
233
234 #[test]
239 fn test_render_mem_roundtrip_multiline_value() {
240 use crate::parser::mem::parse_mem;
241
242 let mut entries = BTreeMap::new();
243 let (k, e) = make_entry(
244 "ml.entry",
245 "first line\nsecond line\nthird line",
246 MemoryTtl::Permanent,
247 MemoryScope::Project,
248 );
249 entries.insert(k, e);
250 let mem = MemFile {
251 format_version: "1.0".to_owned(),
252 package: "test.pkg".to_owned(),
253 updated_at: "2026-04-08T10:00:00Z".to_owned(),
254 entries,
255 };
256 let rendered = render_mem(&mem);
257 let parsed = parse_mem(&rendered).expect("roundtrip parse failed");
258 assert_eq!(mem, parsed);
259 }
260
261 #[test]
266 fn test_render_mem_roundtrip_minimal() {
267 use crate::parser::mem::parse_mem;
268
269 let mem = minimal_mem();
270 let rendered = render_mem(&mem);
271 let parsed = parse_mem(&rendered).expect("roundtrip parse failed");
272 assert_eq!(mem, parsed);
273 }
274
275 #[test]
280 fn test_render_mem_json_valid() {
281 let json = render_mem_json(&full_mem());
282 let parsed: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");
283 assert_eq!(parsed["package"], "acme.migration");
284 assert!(parsed["entries"].is_object());
285 }
286
287 #[test]
292 fn test_render_mem_sql_contains_insert() {
293 let sql = render_mem_sql(&full_mem());
294 assert!(sql.contains("INSERT INTO agm_memory"));
295 assert!(sql.contains("acme.migration"));
296 assert!(sql.contains("project.db_version"));
297 }
298
299 #[test]
300 fn test_render_mem_sql_escapes_single_quotes() {
301 let mut entries = BTreeMap::new();
302 let (k, e) = make_entry(
303 "test.entry",
304 "it's a value",
305 MemoryTtl::Permanent,
306 MemoryScope::Project,
307 );
308 entries.insert(k, e);
309 let mem = MemFile {
310 format_version: "1.0".to_owned(),
311 package: "test.pkg".to_owned(),
312 updated_at: "2026-04-08T10:00:00Z".to_owned(),
313 entries,
314 };
315 let sql = render_mem_sql(&mem);
316 assert!(sql.contains("it''s a value"));
317 }
318
319 #[test]
324 fn test_render_mem_snapshot_minimal() {
325 let output = render_mem(&minimal_mem());
326 insta::assert_snapshot!("render_mem_minimal", output);
327 }
328
329 #[test]
330 fn test_render_mem_snapshot_full() {
331 let output = render_mem(&full_mem());
332 insta::assert_snapshot!("render_mem_full", output);
333 }
334}