1use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7use crate::storage::WritePolicy;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "type", rename_all = "lowercase")]
17pub enum BackendConfig {
18 Local {
20 path: PathBuf,
22 },
23 S3 {
25 bucket: String,
26 region: String,
27 endpoint: Option<String>,
28 },
29 Redis {
31 url: String,
33 #[serde(default)]
35 ttl_secs: Option<u64>,
36 },
37 Pg {
40 url: String,
42 max_conns: u32,
44 },
45 Tiered {
51 l1: Box<BackendConfig>,
53 l2: Box<BackendConfig>,
55 l3: Box<BackendConfig>,
57 #[serde(default)]
59 write_policy: WritePolicy,
60 },
61}
62
63impl Default for BackendConfig {
64 fn default() -> Self {
65 Self::Local {
66 path: PathBuf::from("/var/cache/sui"),
67 }
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn default_backend_is_local() {
77 assert!(matches!(BackendConfig::default(), BackendConfig::Local { .. }));
78 }
79
80 #[test]
81 fn local_backend_roundtrips_through_json() {
82 let cfg = BackendConfig::Local {
83 path: PathBuf::from("/var/cache/sui"),
84 };
85 let json = serde_json::to_string(&cfg).unwrap();
86 assert!(json.contains("local"));
87 let parsed: BackendConfig = serde_json::from_str(&json).unwrap();
88 assert!(matches!(parsed, BackendConfig::Local { .. }));
89 }
90
91 #[test]
92 fn s3_backend_roundtrips_through_json() {
93 let cfg = BackendConfig::S3 {
94 bucket: "my-cache".to_string(),
95 region: "us-east-1".to_string(),
96 endpoint: Some("http://localhost:9000".to_string()),
97 };
98 let json = serde_json::to_string_pretty(&cfg).unwrap();
99 let parsed: BackendConfig = serde_json::from_str(&json).unwrap();
100 assert!(matches!(parsed, BackendConfig::S3 { .. }));
101 }
102
103 #[test]
104 fn redis_ttl_defaults_to_none() {
105 let json = r#"{ "type": "redis", "url": "redis://r:6379" }"#;
106 let parsed: BackendConfig = serde_json::from_str(json).unwrap();
107 assert!(matches!(parsed, BackendConfig::Redis { ttl_secs: None, .. }));
108 }
109
110 #[test]
111 fn tiered_backend_roundtrips_through_json() {
112 let backend = BackendConfig::Tiered {
113 l1: Box::new(BackendConfig::Redis {
114 url: "redis://redis:6379".to_string(),
115 ttl_secs: Some(3600),
116 }),
117 l2: Box::new(BackendConfig::Pg {
118 url: "postgres://pg:5432/sui".to_string(),
119 max_conns: 16,
120 }),
121 l3: Box::new(BackendConfig::S3 {
122 bucket: "sui-super-cache".to_string(),
123 region: "us-east-1".to_string(),
124 endpoint: None,
125 }),
126 write_policy: WritePolicy::WriteThrough,
127 };
128 let json = serde_json::to_string_pretty(&backend).unwrap();
129 assert!(json.contains("tiered"));
130 assert!(json.contains("redis"));
131 assert!(json.contains("write-through"));
132 let parsed: BackendConfig = serde_json::from_str(&json).unwrap();
133 match parsed {
134 BackendConfig::Tiered { l1, l2, l3, write_policy } => {
135 assert!(matches!(*l1, BackendConfig::Redis { .. }));
136 assert!(matches!(*l2, BackendConfig::Pg { .. }));
137 assert!(matches!(*l3, BackendConfig::S3 { .. }));
138 assert_eq!(write_policy, WritePolicy::WriteThrough);
139 }
140 other => panic!("expected tiered, got {other:?}"),
141 }
142 }
143
144 #[test]
145 fn tiered_write_policy_defaults_when_absent() {
146 let json = r#"{
147 "type": "tiered",
148 "l1": { "type": "redis", "url": "redis://r:6379" },
149 "l2": { "type": "pg", "url": "postgres://p:5432/s", "max_conns": 8 },
150 "l3": { "type": "local", "path": "/var/cache/sui" }
151 }"#;
152 let parsed: BackendConfig = serde_json::from_str(json).unwrap();
153 match parsed {
154 BackendConfig::Tiered { write_policy, .. } => {
155 assert_eq!(write_policy, WritePolicy::default());
156 assert_eq!(write_policy, WritePolicy::WriteThrough);
157 }
158 other => panic!("expected tiered, got {other:?}"),
159 }
160 }
161}