ddns_a/state/
mod.rs

1//! IP state persistence for detecting changes across restarts.
2//!
3//! This module provides abstractions for storing and retrieving
4//! adapter snapshot state between program executions.
5
6mod file;
7
8#[cfg(test)]
9#[path = "mod_tests.rs"]
10mod tests;
11
12pub use file::FileStateStore;
13
14use std::io;
15
16use thiserror::Error;
17
18use crate::network::AdapterSnapshot;
19
20/// Result of loading state from persistent storage.
21///
22/// Explicitly models all valid states to avoid ambiguity:
23/// - Successfully loaded previous state
24/// - No previous state exists (first run)
25/// - State exists but is corrupted/unreadable
26#[derive(Debug, Clone)]
27pub enum LoadResult {
28    /// Successfully loaded previously saved snapshots.
29    Loaded(Vec<AdapterSnapshot>),
30
31    /// No state file exists (first run or explicitly deleted).
32    NotFound,
33
34    /// State file exists but could not be parsed.
35    /// Program should continue with fresh state and overwrite on next save.
36    Corrupted {
37        /// Reason for corruption (for logging/debugging).
38        reason: String,
39    },
40}
41
42impl LoadResult {
43    /// Returns the loaded snapshots, or an empty vec for `NotFound`/`Corrupted`.
44    #[must_use]
45    pub fn into_snapshots(self) -> Vec<AdapterSnapshot> {
46        match self {
47            Self::Loaded(snapshots) => snapshots,
48            Self::NotFound | Self::Corrupted { .. } => Vec::new(),
49        }
50    }
51
52    /// Returns `true` if state was successfully loaded.
53    #[must_use]
54    pub const fn is_loaded(&self) -> bool {
55        matches!(self, Self::Loaded(_))
56    }
57}
58
59/// Errors that can occur during state persistence operations.
60///
61/// Only covers write-side errors; read-side issues are modeled
62/// as [`LoadResult`] variants to allow graceful degradation.
63#[derive(Debug, Error)]
64pub enum StateError {
65    /// Failed to write the state file.
66    #[error("Failed to write state file: {0}")]
67    Write(#[source] io::Error),
68
69    /// Failed to serialize state to JSON.
70    #[error("Failed to serialize state: {0}")]
71    Serialize(#[source] serde_json::Error),
72}
73
74/// Abstraction for persisting adapter state between program runs.
75///
76/// Implementations should:
77/// - Use atomic writes to prevent corruption from crashes
78/// - Handle missing files gracefully (return `LoadResult::NotFound`)
79/// - Degrade gracefully on read errors (return `LoadResult::Corrupted`)
80///
81/// # Testing
82///
83/// Use [`MockStateStore`] in tests to avoid filesystem dependencies.
84pub trait StateStore: Send + Sync {
85    /// Loads previously saved state.
86    ///
87    /// Returns one of:
88    /// - `LoadResult::Loaded` - State was successfully loaded
89    /// - `LoadResult::NotFound` - No state file exists
90    /// - `LoadResult::Corrupted` - State file exists but is invalid
91    fn load(&self) -> LoadResult;
92
93    /// Saves current adapter state for future reference.
94    ///
95    /// Implementations should use atomic write semantics (write to temp file,
96    /// then rename) to prevent corruption if the program crashes mid-write.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if the state cannot be written.
101    fn save(
102        &self,
103        snapshots: &[AdapterSnapshot],
104    ) -> impl std::future::Future<Output = Result<(), StateError>> + Send;
105}
106
107/// Mock state store for testing.
108///
109/// Allows tests to inject specific load results and capture saved state.
110#[cfg(test)]
111pub mod mock {
112    use super::*;
113    use std::sync::RwLock;
114
115    /// A mock implementation of [`StateStore`] for testing.
116    #[derive(Debug)]
117    pub struct MockStateStore {
118        load_result: LoadResult,
119        saved: RwLock<Option<Vec<AdapterSnapshot>>>,
120    }
121
122    impl MockStateStore {
123        /// Creates a mock that returns `LoadResult::Loaded` with the given snapshots.
124        #[must_use]
125        pub fn with_loaded(snapshots: Vec<AdapterSnapshot>) -> Self {
126            Self {
127                load_result: LoadResult::Loaded(snapshots),
128                saved: RwLock::new(None),
129            }
130        }
131
132        /// Creates a mock that returns `LoadResult::NotFound`.
133        #[must_use]
134        pub fn not_found() -> Self {
135            Self {
136                load_result: LoadResult::NotFound,
137                saved: RwLock::new(None),
138            }
139        }
140
141        /// Creates a mock that returns `LoadResult::Corrupted`.
142        #[must_use]
143        pub fn corrupted(reason: impl Into<String>) -> Self {
144            Self {
145                load_result: LoadResult::Corrupted {
146                    reason: reason.into(),
147                },
148                saved: RwLock::new(None),
149            }
150        }
151
152        /// Returns the last saved snapshots, if any.
153        ///
154        /// # Panics
155        ///
156        /// Panics if the internal lock is poisoned (only in test code).
157        #[must_use]
158        pub fn saved_snapshots(&self) -> Option<Vec<AdapterSnapshot>> {
159            self.saved.read().unwrap().clone()
160        }
161    }
162
163    impl StateStore for MockStateStore {
164        fn load(&self) -> LoadResult {
165            self.load_result.clone()
166        }
167
168        async fn save(&self, snapshots: &[AdapterSnapshot]) -> Result<(), StateError> {
169            *self.saved.write().unwrap() = Some(snapshots.to_vec());
170            Ok(())
171        }
172    }
173}