use core::fmt;
use serde::Deserialize;
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
pub type NamespaceValues = BTreeMap<String, BTreeMap<String, BTreeMap<String, String>>>;
pub type PushedPairs = BTreeMap<String, BTreeSet<String>>;
#[derive(Default, Clone)]
pub struct ProviderCache {
pub values: NamespaceValues,
pub pushed: PushedPairs,
}
impl fmt::Debug for ProviderCache {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ProviderCache")
.field("namespaces", &self.values.len())
.field("pushed", &self.pushed.len())
.finish()
}
}
#[derive(Default, Deserialize)]
pub(super) struct ProviderCacheFile {
pub(super) version: u32,
#[serde(default)]
pub(super) namespaces: NamespaceValues,
#[serde(default)]
pub(super) pushed: PushedPairs,
}
impl fmt::Debug for ProviderCacheFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ProviderCacheFile")
.field("version", &self.version)
.field("namespaces", &self.namespaces.len())
.field("pushed", &self.pushed.len())
.finish()
}
}
pub const PROVIDER_CACHE_VERSION: u32 = 2;
#[must_use]
pub fn provider_cache_on_disk(path: &Path) -> ProviderCache {
let Ok(raw) = std::fs::read_to_string(path) else {
return ProviderCache::default();
};
match serde_json::from_str::<ProviderCacheFile>(&raw) {
Ok(file) if file.version == PROVIDER_CACHE_VERSION => ProviderCache {
values: file.namespaces,
pushed: file.pushed,
},
_ => ProviderCache::default(),
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use super::super::testing::*;
use super::*;
#[test]
fn provider_cache_on_disk_reads_a_real_cache_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("secrets-cache.json");
std::fs::write(
&path,
r#"{"version":2,"namespaces":{"vercel":{"API_KEY":{"production":"sk_live"}}},"pushed":{"vercel":["production"]}}"#,
)
.unwrap();
let cache = provider_cache_on_disk(&path);
assert_eq!(cache.values["vercel"]["API_KEY"]["production"], "sk_live");
assert_eq!(
cache.pushed["vercel"],
BTreeSet::from(["production".to_string()])
);
}
#[test]
fn provider_cache_on_disk_is_empty_for_a_missing_or_broken_file() {
let dir = tempfile::tempdir().unwrap();
assert!(
provider_cache_on_disk(&dir.path().join("absent.json"))
.values
.is_empty()
);
let broken = dir.path().join("broken.json");
std::fs::write(&broken, "not json").unwrap();
assert!(provider_cache_on_disk(&broken).values.is_empty());
let future = dir.path().join("future.json");
std::fs::write(&future, r#"{"version":999,"namespaces":{}}"#).unwrap();
assert!(provider_cache_on_disk(&future).values.is_empty());
}
#[test]
fn a_provider_cache_debug_never_prints_a_value() {
let cache = vercel_production();
assert_eq!(
format!("{cache:?}"),
"ProviderCache { namespaces: 1, pushed: 1 }"
);
}
}