Skip to main content

cratestack_client_rust/
state.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3use std::sync::Mutex;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8use crate::error::ClientError;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11pub struct RequestJournalEntry {
12    pub method: String,
13    pub path: String,
14    pub status_code: u16,
15    pub content_type: Option<String>,
16    pub recorded_at: DateTime<Utc>,
17}
18
19fn default_schema_version() -> u32 {
20    1
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
24pub struct PersistedClientState {
25    #[serde(default = "default_schema_version")]
26    pub schema_version: u32,
27    #[serde(default)]
28    pub state_version: u64,
29    #[serde(default)]
30    pub request_journal: Vec<RequestJournalEntry>,
31}
32
33impl Default for PersistedClientState {
34    fn default() -> Self {
35        Self {
36            schema_version: default_schema_version(),
37            state_version: 0,
38            request_journal: Vec::new(),
39        }
40    }
41}
42
43pub trait ClientStateStore: Send + Sync {
44    fn load(&self) -> Result<PersistedClientState, ClientError>;
45    fn save(&self, state: &PersistedClientState) -> Result<(), ClientError>;
46
47    fn append_request_journal(&self, entry: &RequestJournalEntry) -> Result<(), ClientError> {
48        let mut state = self.load()?;
49        state.request_journal.push(entry.clone());
50        state.state_version = state.state_version.saturating_add(1);
51        self.save(&state)
52    }
53}
54
55#[derive(Debug, Default)]
56pub struct InMemoryStateStore {
57    state: Mutex<PersistedClientState>,
58}
59
60impl ClientStateStore for InMemoryStateStore {
61    fn load(&self) -> Result<PersistedClientState, ClientError> {
62        self.state
63            .lock()
64            .map_err(|error| ClientError::State(format!("failed to lock state store: {error}")))
65            .map(|state| state.clone())
66    }
67
68    fn save(&self, state: &PersistedClientState) -> Result<(), ClientError> {
69        let mut guard = self
70            .state
71            .lock()
72            .map_err(|error| ClientError::State(format!("failed to lock state store: {error}")))?;
73        *guard = state.clone();
74        Ok(())
75    }
76}
77
78#[derive(Debug, Clone)]
79pub struct JsonFileStateStore {
80    path: PathBuf,
81}
82
83impl JsonFileStateStore {
84    pub fn new(path: impl Into<PathBuf>) -> Self {
85        Self { path: path.into() }
86    }
87
88    pub fn path(&self) -> &Path {
89        &self.path
90    }
91}
92
93impl ClientStateStore for JsonFileStateStore {
94    fn load(&self) -> Result<PersistedClientState, ClientError> {
95        match fs::read(&self.path) {
96            Ok(bytes) => serde_json::from_slice(&bytes).map_err(|error| {
97                ClientError::State(format!(
98                    "failed to decode state file {}: {error}",
99                    self.path.display()
100                ))
101            }),
102            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
103                Ok(PersistedClientState::default())
104            }
105            Err(error) => Err(ClientError::State(format!(
106                "failed to read state file {}: {error}",
107                self.path.display()
108            ))),
109        }
110    }
111
112    fn save(&self, state: &PersistedClientState) -> Result<(), ClientError> {
113        if let Some(parent) = self.path.parent() {
114            fs::create_dir_all(parent).map_err(|error| {
115                ClientError::State(format!(
116                    "failed to create state directory {}: {error}",
117                    parent.display()
118                ))
119            })?;
120        }
121        let bytes = serde_json::to_vec_pretty(state).map_err(|error| {
122            ClientError::State(format!(
123                "failed to encode state file {}: {error}",
124                self.path.display()
125            ))
126        })?;
127        fs::write(&self.path, bytes).map_err(|error| {
128            ClientError::State(format!(
129                "failed to write state file {}: {error}",
130                self.path.display()
131            ))
132        })
133    }
134}