use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use shikumi::TieredConfig;
use crate::push::{NarCodec, XzLevel, ZstdLevel};
pub use sui_castore::BackendConfig;
pub use sui_castore::WritePolicy;
pub const CACHE_TIER_ENV: &str = "SUI_CACHE_TIER";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct CacheConfig {
pub listen: String,
pub backend: BackendConfig,
pub signing_key: Option<PathBuf>,
pub priority: u32,
pub want_mass_query: bool,
pub store_dir: String,
#[serde(default)]
pub require_sigs: bool,
#[serde(default)]
pub nar_codec: NarCodec,
}
impl Default for CacheConfig {
fn default() -> Self {
<Self as TieredConfig>::prescribed_default()
}
}
impl CacheConfig {
#[must_use]
pub fn resolve() -> Self {
<Self as TieredConfig>::resolve_from_env(CACHE_TIER_ENV)
}
}
impl TieredConfig for CacheConfig {
fn bare() -> Self {
Self {
listen: "0.0.0.0:5000".to_string(),
backend: BackendConfig::default(),
signing_key: None,
priority: 40,
want_mass_query: true,
store_dir: "/nix/store".to_string(),
require_sigs: false,
nar_codec: NarCodec::Xz {
level: XzLevel::default(),
},
}
}
fn prescribed_default() -> Self {
Self {
nar_codec: NarCodec::Zstd {
level: ZstdLevel::default(),
},
..Self::bare()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use shikumi::ConfigTier;
use std::path::PathBuf;
#[test]
fn default_config_has_sane_values() {
let config = CacheConfig::default();
assert_eq!(config.listen, "0.0.0.0:5000");
assert_eq!(config.store_dir, "/nix/store");
assert_eq!(config.priority, 40);
assert!(config.want_mass_query);
assert!(config.signing_key.is_none());
}
#[test]
fn default_backend_is_local() {
let config = CacheConfig::default();
assert!(matches!(config.backend, BackendConfig::Local { .. }));
}
#[test]
fn config_serializes_to_json() {
let config = CacheConfig::default();
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("local"));
assert!(json.contains("5000"));
}
#[test]
fn config_roundtrips_through_json() {
let config = CacheConfig {
listen: "127.0.0.1:8080".to_string(),
backend: BackendConfig::S3 {
bucket: "my-cache".to_string(),
region: "us-east-1".to_string(),
endpoint: Some("http://localhost:9000".to_string()),
},
signing_key: Some(PathBuf::from("/tmp/key.sec")),
priority: 30,
want_mass_query: false,
store_dir: "/nix/store".to_string(),
require_sigs: true,
nar_codec: NarCodec::default(),
};
let json = serde_json::to_string_pretty(&config).unwrap();
let parsed: CacheConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.listen, "127.0.0.1:8080");
assert_eq!(parsed.priority, 30);
assert!(!parsed.want_mass_query);
assert!(parsed.require_sigs);
assert!(matches!(parsed.backend, BackendConfig::S3 { .. }));
}
#[test]
fn require_sigs_defaults_to_false_when_absent() {
let json = r#"{
"listen": "0.0.0.0:5000",
"backend": { "type": "local", "path": "/var/cache/sui" },
"signing_key": null,
"priority": 40,
"want_mass_query": true,
"store_dir": "/nix/store"
}"#;
let parsed: CacheConfig = serde_json::from_str(json).unwrap();
assert!(!parsed.require_sigs);
}
#[test]
fn tiered_backend_roundtrips_through_json() {
let backend = BackendConfig::Tiered {
l1: Box::new(BackendConfig::Redis {
url: "redis://redis:6379".to_string(),
ttl_secs: Some(3600),
}),
l2: Box::new(BackendConfig::Pg {
url: "postgres://pg:5432/sui".to_string(),
max_conns: 16,
}),
l3: Box::new(BackendConfig::S3 {
bucket: "sui-super-cache".to_string(),
region: "us-east-1".to_string(),
endpoint: None,
}),
write_policy: WritePolicy::WriteThrough,
};
let json = serde_json::to_string_pretty(&backend).unwrap();
assert!(json.contains("tiered"));
assert!(json.contains("redis"));
assert!(json.contains("write-through"));
let parsed: BackendConfig = serde_json::from_str(&json).unwrap();
match parsed {
BackendConfig::Tiered {
l1,
l2,
l3,
write_policy,
} => {
assert!(matches!(*l1, BackendConfig::Redis { .. }));
assert!(matches!(*l2, BackendConfig::Pg { .. }));
assert!(matches!(*l3, BackendConfig::S3 { .. }));
assert_eq!(write_policy, WritePolicy::WriteThrough);
}
other => panic!("expected tiered, got {other:?}"),
}
}
#[test]
fn tiered_write_policy_defaults_when_absent() {
let json = r#"{
"type": "tiered",
"l1": { "type": "redis", "url": "redis://r:6379" },
"l2": { "type": "pg", "url": "postgres://p:5432/s", "max_conns": 8 },
"l3": { "type": "local", "path": "/var/cache/sui" }
}"#;
let parsed: BackendConfig = serde_json::from_str(json).unwrap();
match parsed {
BackendConfig::Tiered { write_policy, .. } => {
assert_eq!(write_policy, WritePolicy::default());
assert_eq!(write_policy, WritePolicy::WriteThrough);
}
other => panic!("expected tiered, got {other:?}"),
}
}
#[test]
fn the_prescribed_tier_is_the_measured_fast_path() {
assert_eq!(
CacheConfig::prescribed_default().nar_codec,
NarCodec::Zstd {
level: ZstdLevel::default()
}
);
assert_eq!(CacheConfig::default(), CacheConfig::prescribed_default());
}
#[test]
fn the_bare_tier_is_the_documented_past() {
assert_eq!(
CacheConfig::bare().nar_codec,
NarCodec::Xz {
level: XzLevel::default()
}
);
let promoted = CacheConfig {
nar_codec: CacheConfig::prescribed_default().nar_codec,
..CacheConfig::bare()
};
assert_eq!(promoted, CacheConfig::prescribed_default());
}
#[test]
fn a_partial_yaml_overlay_changes_one_field_and_keeps_the_rest() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("cache.yaml");
std::fs::write(&path, "nar_codec:\n codec: xz\n level: 9\n").unwrap();
let resolved = CacheConfig::resolve_tier(ConfigTier::Custom(path));
assert_eq!(
resolved.nar_codec,
NarCodec::Xz {
level: XzLevel::new(9).unwrap()
},
"the overlay must reach the codec"
);
assert_eq!(
resolved.listen,
CacheConfig::prescribed_default().listen,
"an unmentioned field must keep its prescribed value"
);
assert_eq!(
resolved.priority,
CacheConfig::prescribed_default().priority
);
}
#[test]
fn a_yaml_overlay_may_omit_the_codec_and_keep_the_fast_path() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("cache.yaml");
std::fs::write(&path, "priority: 10\n").unwrap();
let resolved = CacheConfig::resolve_tier(ConfigTier::Custom(path));
assert_eq!(resolved.priority, 10);
assert_eq!(
resolved.nar_codec,
CacheConfig::prescribed_default().nar_codec,
"not naming a codec must leave the fast default in place"
);
}
#[test]
fn the_named_tiers_resolve_to_their_tier_methods() {
assert_eq!(
CacheConfig::resolve_tier(ConfigTier::Bare),
CacheConfig::bare()
);
assert_eq!(
CacheConfig::resolve_tier(ConfigTier::Default),
CacheConfig::prescribed_default()
);
}
#[test]
fn the_config_round_trips_the_codec_through_json() {
let cfg = CacheConfig {
nar_codec: NarCodec::Xz {
level: XzLevel::new(3).unwrap(),
},
..CacheConfig::default()
};
let parsed: CacheConfig =
serde_json::from_str(&serde_json::to_string(&cfg).unwrap()).unwrap();
assert_eq!(parsed, cfg);
}
#[test]
fn redis_ttl_defaults_to_none() {
let json = r#"{ "type": "redis", "url": "redis://r:6379" }"#;
let parsed: BackendConfig = serde_json::from_str(json).unwrap();
assert!(matches!(
parsed,
BackendConfig::Redis { ttl_secs: None, .. }
));
}
}