weave_content/
writeback.rs1use std::path::Path;
2
3#[derive(Debug)]
5pub struct PendingId {
6 pub line: usize,
8 pub id: String,
10 pub kind: WriteBackKind,
12}
13
14#[derive(Debug)]
16pub enum WriteBackKind {
17 EntityFrontMatter,
20 CaseId,
23 InlineEvent,
26 Relationship,
29}
30
31pub fn apply_writebacks(content: &str, pending: &mut [PendingId]) -> Option<String> {
38 if pending.is_empty() {
39 return None;
40 }
41
42 pending.sort_by_key(|p| std::cmp::Reverse(p.line));
44
45 let mut lines: Vec<String> = content.lines().map(String::from).collect();
46 let trailing_newline = content.ends_with('\n');
48
49 for p in pending.iter() {
50 let insert_text = match p.kind {
51 WriteBackKind::EntityFrontMatter => {
52 format!("id: {}", p.id)
53 }
54 WriteBackKind::CaseId => {
55 format!("id: {}", p.id)
56 }
57 WriteBackKind::InlineEvent => {
58 format!("- id: {}", p.id)
59 }
60 WriteBackKind::Relationship => {
61 format!(" - id: {}", p.id)
62 }
63 };
64
65 let insert_after = match p.kind {
66 WriteBackKind::EntityFrontMatter | WriteBackKind::CaseId => {
67 let idx = p.line.saturating_sub(1); if idx <= lines.len() {
72 lines.insert(idx, insert_text);
73 }
74 continue;
75 }
76 WriteBackKind::InlineEvent | WriteBackKind::Relationship => {
77 p.line }
80 };
81
82 if insert_after <= lines.len() {
83 lines.insert(insert_after, insert_text);
84 }
85 }
86
87 let mut result = lines.join("\n");
88 if trailing_newline {
89 result.push('\n');
90 }
91 Some(result)
92}
93
94pub fn write_file(path: &Path, content: &str) -> Result<(), String> {
96 std::fs::write(path, content)
97 .map_err(|e| format!("{}: error writing file: {e}", path.display()))
98}
99
100pub fn find_front_matter_end(content: &str) -> Option<usize> {
104 let mut in_front_matter = false;
105 for (i, line) in content.lines().enumerate() {
106 let trimmed = line.trim();
107 if trimmed == "---" && !in_front_matter {
108 in_front_matter = true;
109 } else if trimmed == "---" && in_front_matter {
110 return Some(i + 1); }
112 }
113 None
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn writeback_entity_front_matter() {
122 let content = "---\n---\n\n# Mark Bonnick\n\n- nationality: British\n";
123 let end_line = find_front_matter_end(content).unwrap();
124 assert_eq!(end_line, 2);
125
126 let mut pending = vec![PendingId {
127 line: end_line,
128 id: "01JXYZ123456789ABCDEFGHIJK".to_string(),
129 kind: WriteBackKind::EntityFrontMatter,
130 }];
131
132 let result = apply_writebacks(content, &mut pending).unwrap();
133 assert!(result.contains("id: 01JXYZ123456789ABCDEFGHIJK"));
134 let lines: Vec<&str> = result.lines().collect();
136 assert_eq!(lines[0], "---");
137 assert_eq!(lines[1], "id: 01JXYZ123456789ABCDEFGHIJK");
138 assert_eq!(lines[2], "---");
139 }
140
141 #[test]
142 fn writeback_entity_front_matter_with_existing_fields() {
143 let content = "---\nother: value\n---\n\n# Test\n";
144 let end_line = find_front_matter_end(content).unwrap();
145 assert_eq!(end_line, 3);
146
147 let mut pending = vec![PendingId {
148 line: end_line,
149 id: "01JABC000000000000000000AA".to_string(),
150 kind: WriteBackKind::EntityFrontMatter,
151 }];
152
153 let result = apply_writebacks(content, &mut pending).unwrap();
154 let lines: Vec<&str> = result.lines().collect();
155 assert_eq!(lines[0], "---");
156 assert_eq!(lines[1], "other: value");
157 assert_eq!(lines[2], "id: 01JABC000000000000000000AA");
158 assert_eq!(lines[3], "---");
159 }
160
161 #[test]
162 fn writeback_inline_event() {
163 let content = "\
164## Events
165
166### Dismissal
167- occurred_at: 2024-12-24
168- event_type: termination
169";
170 let mut pending = vec![PendingId {
172 line: 3,
173 id: "01JXYZ123456789ABCDEFGHIJK".to_string(),
174 kind: WriteBackKind::InlineEvent,
175 }];
176
177 let result = apply_writebacks(content, &mut pending).unwrap();
178 let lines: Vec<&str> = result.lines().collect();
179 assert_eq!(lines[2], "### Dismissal");
180 assert_eq!(lines[3], "- id: 01JXYZ123456789ABCDEFGHIJK");
181 assert_eq!(lines[4], "- occurred_at: 2024-12-24");
182 }
183
184 #[test]
185 fn writeback_relationship() {
186 let content = "\
187## Relationships
188
189- Alice -> Bob: employed_by
190 - source: https://example.com
191";
192 let mut pending = vec![PendingId {
194 line: 3,
195 id: "01JXYZ123456789ABCDEFGHIJK".to_string(),
196 kind: WriteBackKind::Relationship,
197 }];
198
199 let result = apply_writebacks(content, &mut pending).unwrap();
200 let lines: Vec<&str> = result.lines().collect();
201 assert_eq!(lines[2], "- Alice -> Bob: employed_by");
202 assert_eq!(lines[3], " - id: 01JXYZ123456789ABCDEFGHIJK");
203 assert_eq!(lines[4], " - source: https://example.com");
204 }
205
206 #[test]
207 fn writeback_multiple_insertions() {
208 let content = "\
209## Events
210
211### Event A
212- occurred_at: 2024-01-01
213
214### Event B
215- occurred_at: 2024-06-01
216
217## Relationships
218
219- Event A -> Event B: associate_of
220";
221 let mut pending = vec![
222 PendingId {
223 line: 3,
224 id: "01JAAA000000000000000000AA".to_string(),
225 kind: WriteBackKind::InlineEvent,
226 },
227 PendingId {
228 line: 6,
229 id: "01JBBB000000000000000000BB".to_string(),
230 kind: WriteBackKind::InlineEvent,
231 },
232 PendingId {
233 line: 10,
234 id: "01JCCC000000000000000000CC".to_string(),
235 kind: WriteBackKind::Relationship,
236 },
237 ];
238
239 let result = apply_writebacks(content, &mut pending).unwrap();
240 assert!(result.contains("- id: 01JAAA000000000000000000AA"));
241 assert!(result.contains("- id: 01JBBB000000000000000000BB"));
242 assert!(result.contains(" - id: 01JCCC000000000000000000CC"));
243 }
244
245 #[test]
246 fn writeback_empty_pending() {
247 let content = "some content\n";
248 let mut pending = Vec::new();
249 assert!(apply_writebacks(content, &mut pending).is_none());
250 }
251
252 #[test]
253 fn writeback_preserves_trailing_newline() {
254 let content = "---\n---\n\n# Test\n";
255 let mut pending = vec![PendingId {
256 line: 2,
257 id: "01JABC000000000000000000AA".to_string(),
258 kind: WriteBackKind::EntityFrontMatter,
259 }];
260 let result = apply_writebacks(content, &mut pending).unwrap();
261 assert!(result.ends_with('\n'));
262 }
263
264 #[test]
265 fn writeback_case_id() {
266 let content = "---\nsources:\n - https://example.com\n---\n\n# Some Case\n";
267 let end_line = find_front_matter_end(content).unwrap();
268 assert_eq!(end_line, 4);
269
270 let mut pending = vec![PendingId {
271 line: end_line,
272 id: "01JXYZ123456789ABCDEFGHIJK".to_string(),
273 kind: WriteBackKind::CaseId,
274 }];
275
276 let result = apply_writebacks(content, &mut pending).unwrap();
277 let lines: Vec<&str> = result.lines().collect();
278 assert_eq!(lines[0], "---");
279 assert_eq!(lines[3], "id: 01JXYZ123456789ABCDEFGHIJK");
280 assert_eq!(lines[4], "---");
281 assert!(!result.contains("\nnulid:"));
282 }
283
284 #[test]
285 fn find_front_matter_end_basic() {
286 assert_eq!(find_front_matter_end("---\nid: foo\n---\n"), Some(3));
287 assert_eq!(find_front_matter_end("---\n---\n"), Some(2));
288 assert_eq!(find_front_matter_end("no front matter"), None);
289 assert_eq!(find_front_matter_end("---\nunclosed"), None);
290 }
291}