1use crate::PersistenceResult;
2use async_trait::async_trait;
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::path::{Path, PathBuf};
6use uuid::Uuid;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct PersistenceMetadata {
11 pub instance_id: Uuid,
13 pub saved_at: DateTime<Utc>,
15 #[serde(default)]
18 pub writer_version: Option<String>,
19 #[serde(default)]
22 pub writer_commit: Option<String>,
23 #[serde(default, skip_serializing)]
28 pub format_version: Option<u32>,
29}
30
31impl PersistenceMetadata {
32 pub fn new(instance_id: Uuid) -> Self {
33 Self {
34 instance_id,
35 saved_at: Utc::now(),
36 writer_version: None,
37 writer_commit: None,
38 format_version: None,
39 }
40 }
41
42 pub fn with_writer_stamp(
45 mut self,
46 version: impl Into<String>,
47 commit: impl Into<String>,
48 ) -> Self {
49 self.writer_version = Some(version.into());
50 self.writer_commit = Some(commit.into());
51 self
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct StoreSnapshot {
58 pub data: Vec<u8>,
60 pub metadata: PersistenceMetadata,
62}
63
64#[derive(Debug, Clone)]
66pub enum PersistenceEvent {
67 Saved(PersistenceMetadata),
69 ExternalChangeDetected {
71 path: PathBuf,
72 saved_at: DateTime<Utc>,
73 },
74 ConflictDetected { reason: String },
76 Error(String),
78}
79
80#[async_trait]
83pub trait PersistenceStore: Send + Sync {
84 async fn save(&self, snapshot: StoreSnapshot) -> PersistenceResult<PersistenceMetadata>;
86
87 async fn load(&self) -> PersistenceResult<(StoreSnapshot, PersistenceMetadata)>;
89
90 async fn exists(&self) -> bool;
92
93 fn path(&self) -> &Path;
95
96 fn instance_id(&self) -> uuid::Uuid;
98
99 #[allow(clippy::type_complexity)]
105 fn load_sync(&self) -> PersistenceResult<Option<(StoreSnapshot, PersistenceMetadata)>> {
106 Err(crate::PersistenceError::Unsupported(
107 "load_sync not supported by this backend".into(),
108 ))
109 }
110
111 async fn close(&self) {}
120}
121
122#[async_trait]
125pub trait ChangeDetector: Send + Sync {
126 async fn start_watching(&self, path: PathBuf) -> PersistenceResult<()>;
128
129 async fn stop_watching(&self) -> PersistenceResult<()>;
131
132 fn subscribe(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent>;
135
136 fn is_watching(&self) -> bool;
138}
139
140#[derive(Debug, Clone)]
142pub struct ChangeEvent {
143 pub path: PathBuf,
145 pub detected_at: DateTime<Utc>,
147}
148
149pub trait Serializer<T: Send + Sync>: Send + Sync {
152 fn serialize(&self, data: &T) -> PersistenceResult<Vec<u8>>;
154
155 fn deserialize(&self, bytes: &[u8]) -> PersistenceResult<T>;
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
161pub enum FormatVersion {
162 V1,
163 V2,
164 V3,
165 V4,
166 V5,
167 V6,
171 V7,
176}
177
178impl FormatVersion {
179 pub const MAX: Self = Self::V7;
181
182 pub fn as_u32(self) -> u32 {
183 match self {
184 Self::V1 => 1,
185 Self::V2 => 2,
186 Self::V3 => 3,
187 Self::V4 => 4,
188 Self::V5 => 5,
189 Self::V6 => 6,
190 Self::V7 => 7,
191 }
192 }
193
194 pub fn from_u32(v: u32) -> Option<Self> {
195 match v {
196 1 => Some(Self::V1),
197 2 => Some(Self::V2),
198 3 => Some(Self::V3),
199 4 => Some(Self::V4),
200 5 => Some(Self::V5),
201 6 => Some(Self::V6),
202 7 => Some(Self::V7),
203 _ => None,
204 }
205 }
206}
207
208#[async_trait]
210pub trait MigrationStrategy: Send + Sync {
211 async fn detect_version(&self, path: &Path) -> PersistenceResult<FormatVersion>;
213
214 async fn migrate(
217 &self,
218 from: FormatVersion,
219 to: FormatVersion,
220 path: &Path,
221 ) -> PersistenceResult<PathBuf>;
222}
223
224pub trait ConflictResolver: Send + Sync {
226 fn should_use_external(
229 &self,
230 local_metadata: &PersistenceMetadata,
231 external_metadata: &PersistenceMetadata,
232 ) -> bool;
233
234 fn explain_resolution(
236 &self,
237 local_metadata: &PersistenceMetadata,
238 external_metadata: &PersistenceMetadata,
239 ) -> String;
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn test_format_version_max_equals_v7() {
248 assert_eq!(FormatVersion::MAX, FormatVersion::V7);
249 }
250
251 #[test]
252 fn test_format_version_max_as_u32_matches_largest_variant() {
253 assert_eq!(FormatVersion::MAX.as_u32(), 7);
254 }
255
256 #[test]
257 fn test_default_metadata_has_no_writer_stamp() {
258 let md = PersistenceMetadata::new(Uuid::nil());
259 assert!(md.writer_version.is_none());
260 assert!(md.writer_commit.is_none());
261 }
262
263 #[test]
264 fn test_with_writer_stamp_populates_both_fields() {
265 let md = PersistenceMetadata::new(Uuid::nil()).with_writer_stamp("0.6.0", "abc1234");
266 assert_eq!(md.writer_version.as_deref(), Some("0.6.0"));
267 assert_eq!(md.writer_commit.as_deref(), Some("abc1234"));
268 }
269
270 #[test]
271 fn test_metadata_serde_round_trips_with_writer_stamp() {
272 let md = PersistenceMetadata::new(Uuid::nil()).with_writer_stamp("0.6.0", "abc1234");
273 let json = serde_json::to_string(&md).unwrap();
274 let parsed: PersistenceMetadata = serde_json::from_str(&json).unwrap();
275 assert_eq!(parsed.writer_version.as_deref(), Some("0.6.0"));
276 assert_eq!(parsed.writer_commit.as_deref(), Some("abc1234"));
277 }
278
279 #[test]
280 fn test_metadata_deserialize_legacy_envelope_missing_stamp_fields() {
281 let legacy = serde_json::json!({
283 "instance_id": "550e8400-e29b-41d4-a716-446655440000",
284 "saved_at": "2024-01-01T00:00:00Z"
285 });
286 let parsed: PersistenceMetadata = serde_json::from_value(legacy).unwrap();
287 assert!(parsed.writer_version.is_none());
288 assert!(parsed.writer_commit.is_none());
289 }
290}