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