cratestack_client_rust/
state.rs1use 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}