use core::fmt;
use serde::Deserialize;
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
pub type NamespaceTable = BTreeMap<String, BTreeMap<String, String>>;
pub type NamespaceValues = BTreeMap<String, NamespaceTable>;
pub type PushedPairs = BTreeMap<String, BTreeSet<String>>;
#[derive(Default, Clone)]
pub struct ProviderCache {
values: NamespaceValues,
pub pushed: PushedPairs,
}
impl ProviderCache {
#[must_use]
pub fn new(values: NamespaceValues, pushed: PushedPairs) -> Self {
Self { values, pushed }
}
#[must_use]
pub fn namespace(&self, name: &str) -> Option<&NamespaceTable> {
self.values.get(name)
}
pub fn namespaces(&self) -> impl Iterator<Item = (&String, &NamespaceTable)> {
self.values.iter()
}
#[must_use]
pub fn namespace_count(&self) -> usize {
self.values.len()
}
#[must_use]
pub fn into_parts(self) -> (NamespaceValues, PushedPairs) {
(self.values, self.pushed)
}
}
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::new(file.namespaces, 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.namespace("vercel").unwrap()["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_eq!(
provider_cache_on_disk(&dir.path().join("absent.json")).namespace_count(),
0
);
let broken = dir.path().join("broken.json");
std::fs::write(&broken, "not json").unwrap();
assert_eq!(provider_cache_on_disk(&broken).namespace_count(), 0);
let future = dir.path().join("future.json");
std::fs::write(&future, r#"{"version":999,"namespaces":{}}"#).unwrap();
assert_eq!(provider_cache_on_disk(&future).namespace_count(), 0);
}
#[test]
fn a_provider_cache_debug_never_prints_a_value() {
let cache = vercel_production();
assert_eq!(
format!("{cache:?}"),
"ProviderCache { namespaces: 1, pushed: 1 }"
);
}
}