|
| 1 | +use std::fs; |
| 2 | +use std::path::PathBuf; |
| 3 | + |
| 4 | +use serde_json::json; |
| 5 | +use tree_ring_memory_core::{decode_jsonl, normalize_import_events}; |
| 6 | +use tree_ring_memory_sqlite::{ExportReport, ImportReport, SQLiteMemoryStore}; |
| 7 | + |
| 8 | +use super::ActionResult; |
| 9 | + |
| 10 | +#[derive(Debug, Clone, PartialEq, Eq)] |
| 11 | +pub struct ExportActionRequest { |
| 12 | + pub output: Option<PathBuf>, |
| 13 | + pub include_sensitive: bool, |
| 14 | + pub include_superseded: bool, |
| 15 | +} |
| 16 | + |
| 17 | +#[derive(Debug, Clone, PartialEq, Eq)] |
| 18 | +pub struct ExportActionReport { |
| 19 | + pub jsonl: Option<String>, |
| 20 | + pub output: Option<PathBuf>, |
| 21 | + pub report: ExportReport, |
| 22 | +} |
| 23 | + |
| 24 | +#[derive(Debug, Clone, PartialEq, Eq)] |
| 25 | +pub struct ImportActionRequest { |
| 26 | + pub path: PathBuf, |
| 27 | + pub dry_run: bool, |
| 28 | + pub replace_existing: bool, |
| 29 | +} |
| 30 | + |
| 31 | +#[derive(Debug, Clone, PartialEq, Eq)] |
| 32 | +pub struct ImportActionReport { |
| 33 | + pub path: PathBuf, |
| 34 | + pub report: ImportReport, |
| 35 | +} |
| 36 | + |
| 37 | +pub fn export_jsonl( |
| 38 | + store: &SQLiteMemoryStore, |
| 39 | + request: ExportActionRequest, |
| 40 | +) -> ActionResult<ExportActionReport> { |
| 41 | + let (jsonl, report) = store |
| 42 | + .export_jsonl(request.include_sensitive, request.include_superseded) |
| 43 | + .map_err(|err| err.to_string())?; |
| 44 | + if let Some(output) = request.output { |
| 45 | + if let Some(parent) = output.parent() { |
| 46 | + if !parent.as_os_str().is_empty() { |
| 47 | + fs::create_dir_all(parent).map_err(|err| err.to_string())?; |
| 48 | + } |
| 49 | + } |
| 50 | + fs::write(&output, jsonl).map_err(|err| err.to_string())?; |
| 51 | + Ok(ExportActionReport { |
| 52 | + jsonl: None, |
| 53 | + output: Some(output), |
| 54 | + report, |
| 55 | + }) |
| 56 | + } else { |
| 57 | + Ok(ExportActionReport { |
| 58 | + jsonl: Some(jsonl), |
| 59 | + output: None, |
| 60 | + report, |
| 61 | + }) |
| 62 | + } |
| 63 | +} |
| 64 | + |
| 65 | +pub fn import_jsonl( |
| 66 | + store: Option<&mut SQLiteMemoryStore>, |
| 67 | + request: ImportActionRequest, |
| 68 | +) -> ActionResult<ImportActionReport> { |
| 69 | + let input = fs::read_to_string(&request.path).map_err(|err| err.to_string())?; |
| 70 | + let report = if request.dry_run { |
| 71 | + let decoded = decode_jsonl(&input).map_err(|err| err.to_string())?; |
| 72 | + let events = normalize_import_events(decoded.events).map_err(|err| err.to_string())?; |
| 73 | + ImportReport { |
| 74 | + valid_count: events.len(), |
| 75 | + inserted_count: 0, |
| 76 | + replaced_count: 0, |
| 77 | + skipped_duplicate_count: 0, |
| 78 | + dry_run: true, |
| 79 | + } |
| 80 | + } else { |
| 81 | + let store = store.ok_or_else(|| { |
| 82 | + "import action requires an open writable store when dry_run=false".to_string() |
| 83 | + })?; |
| 84 | + store |
| 85 | + .import_jsonl(&input, false, request.replace_existing) |
| 86 | + .map_err(|err| err.to_string())? |
| 87 | + }; |
| 88 | + Ok(ImportActionReport { |
| 89 | + path: request.path, |
| 90 | + report, |
| 91 | + }) |
| 92 | +} |
| 93 | + |
| 94 | +pub fn import_json_payload(report: &ImportActionReport) -> serde_json::Value { |
| 95 | + json!({ |
| 96 | + "ok": true, |
| 97 | + "path": report.path, |
| 98 | + "valid_count": report.report.valid_count, |
| 99 | + "inserted_count": report.report.inserted_count, |
| 100 | + "replaced_count": report.report.replaced_count, |
| 101 | + "skipped_duplicate_count": report.report.skipped_duplicate_count, |
| 102 | + "dry_run": report.report.dry_run, |
| 103 | + }) |
| 104 | +} |
| 105 | + |
| 106 | +#[cfg(test)] |
| 107 | +mod tests { |
| 108 | + use tempfile::tempdir; |
| 109 | + use tree_ring_memory_core::MemoryEvent; |
| 110 | + |
| 111 | + use super::*; |
| 112 | + |
| 113 | + #[test] |
| 114 | + fn export_action_can_return_stdout_jsonl() { |
| 115 | + let dir = tempdir().unwrap(); |
| 116 | + let mut store = SQLiteMemoryStore::open(dir.path().join("memory.sqlite")).unwrap(); |
| 117 | + store |
| 118 | + .put(&MemoryEvent::new("Export through shared action.", "lesson").unwrap()) |
| 119 | + .unwrap(); |
| 120 | + |
| 121 | + let report = export_jsonl( |
| 122 | + &store, |
| 123 | + ExportActionRequest { |
| 124 | + output: None, |
| 125 | + include_sensitive: false, |
| 126 | + include_superseded: false, |
| 127 | + }, |
| 128 | + ) |
| 129 | + .unwrap(); |
| 130 | + |
| 131 | + assert_eq!(report.report.memory_count, 1); |
| 132 | + assert!(report.jsonl.unwrap().contains("memory_event")); |
| 133 | + } |
| 134 | + |
| 135 | + #[test] |
| 136 | + fn import_action_dry_run_validates_without_writing() { |
| 137 | + let dir = tempdir().unwrap(); |
| 138 | + let mut source = SQLiteMemoryStore::open(dir.path().join("source.sqlite")).unwrap(); |
| 139 | + let target = SQLiteMemoryStore::open(dir.path().join("target.sqlite")).unwrap(); |
| 140 | + source |
| 141 | + .put(&MemoryEvent::new("Import through shared action.", "lesson").unwrap()) |
| 142 | + .unwrap(); |
| 143 | + let (jsonl, _) = source.export_jsonl(false, false).unwrap(); |
| 144 | + let input = dir.path().join("input.jsonl"); |
| 145 | + fs::write(&input, jsonl).unwrap(); |
| 146 | + |
| 147 | + let report = import_jsonl( |
| 148 | + None, |
| 149 | + ImportActionRequest { |
| 150 | + path: input, |
| 151 | + dry_run: true, |
| 152 | + replace_existing: false, |
| 153 | + }, |
| 154 | + ) |
| 155 | + .unwrap(); |
| 156 | + |
| 157 | + assert_eq!(report.report.valid_count, 1); |
| 158 | + assert_eq!(target.list_all(true).unwrap().len(), 0); |
| 159 | + } |
| 160 | +} |
0 commit comments