use std::path::Path;
use serde::{Deserialize, Serialize};
use super::state::WorkspaceKey;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PersistedState {
pub format_version: u32,
pub keys: Vec<WorkspaceKey>,
}
impl PersistedState {
pub const FORMAT_VERSION: u32 = 2;
#[must_use]
pub fn new_v2(keys: Vec<WorkspaceKey>) -> Self {
Self {
format_version: Self::FORMAT_VERSION,
keys,
}
}
}
#[derive(Debug, Clone, Deserialize)]
struct PersistedStateV1 {
#[allow(dead_code)] format_version: u32,
keys: Vec<WorkspaceKey>,
}
#[derive(Debug, thiserror::Error)]
pub enum PersistedStateError {
#[error("failed to read persisted state from {path}: {source}", path = path.display())]
Io {
path: std::path::PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to parse persisted state JSON: {0}")]
Parse(#[from] serde_json::Error),
#[error("persisted state format_version {found} is newer than supported {supported}")]
UnsupportedFutureVersion {
found: u32,
supported: u32,
},
}
pub fn load_persisted_state(path: &Path) -> Result<PersistedState, PersistedStateError> {
let bytes = std::fs::read(path).map_err(|source| PersistedStateError::Io {
path: path.to_path_buf(),
source,
})?;
parse_persisted_state(&bytes)
}
pub fn parse_persisted_state(bytes: &[u8]) -> Result<PersistedState, PersistedStateError> {
#[derive(Deserialize)]
struct VersionPeek {
format_version: Option<u32>,
}
let peek: VersionPeek = serde_json::from_slice(bytes)?;
let version = peek.format_version.unwrap_or(1);
match version {
v if v == PersistedState::FORMAT_VERSION => Ok(serde_json::from_slice(bytes)?),
1 => {
let v1: PersistedStateV1 = serde_json::from_slice(bytes)?;
Ok(PersistedState::new_v2(v1.keys))
}
future if future > PersistedState::FORMAT_VERSION => {
Err(PersistedStateError::UnsupportedFutureVersion {
found: future,
supported: PersistedState::FORMAT_VERSION,
})
}
_ => Err(PersistedStateError::Parse(serde::de::Error::invalid_value(
serde::de::Unexpected::Unsigned(u64::from(version)),
&"format_version 1 or 2",
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use sqry_core::project::ProjectRootMode;
#[test]
fn parse_v2_round_trips() {
let original = PersistedState::new_v2(vec![WorkspaceKey::new(
PathBuf::from("/repos/example"),
ProjectRootMode::GitRoot,
0xabcd_ef01,
)]);
let wire = serde_json::to_vec(&original).expect("serialise v2");
let back = parse_persisted_state(&wire).expect("parse v2");
assert_eq!(back, original);
}
#[test]
fn parse_v1_upconverts_to_v2_with_workspace_id_none() {
let v1 = serde_json::json!({
"format_version": 1,
"keys": [
{
"index_root": "/repos/example",
"root_mode": "gitRoot",
"config_fingerprint": 0
}
]
});
let wire = serde_json::to_vec(&v1).unwrap();
let upconverted = parse_persisted_state(&wire).expect("upconvert v1");
assert_eq!(upconverted.format_version, PersistedState::FORMAT_VERSION);
assert_eq!(upconverted.keys.len(), 1);
assert!(
upconverted.keys[0].workspace_id.is_none(),
"v1 upconvert must inject workspace_id = None"
);
assert_eq!(
upconverted.keys[0].source_root,
PathBuf::from("/repos/example")
);
}
#[test]
fn parse_unsupported_future_version_errors() {
let future = serde_json::json!({
"format_version": 99,
"keys": []
});
let wire = serde_json::to_vec(&future).unwrap();
let err = parse_persisted_state(&wire).expect_err("must reject future version");
assert!(matches!(
err,
PersistedStateError::UnsupportedFutureVersion {
found: 99,
supported: 2
}
));
}
}