1use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7use crate::storage::WritePolicy;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct CacheConfig {
12 pub listen: String,
14 pub backend: BackendConfig,
16 pub signing_key: Option<PathBuf>,
22 pub priority: u32,
24 pub want_mass_query: bool,
26 pub store_dir: String,
28 #[serde(default)]
38 pub require_sigs: bool,
39}
40
41impl Default for CacheConfig {
42 fn default() -> Self {
43 Self {
44 listen: "0.0.0.0:5000".to_string(),
45 backend: BackendConfig::default(),
46 signing_key: None,
47 priority: 40,
48 want_mass_query: true,
49 store_dir: "/nix/store".to_string(),
50 require_sigs: false,
51 }
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(tag = "type", rename_all = "lowercase")]
58pub enum BackendConfig {
59 Local {
61 path: PathBuf,
63 },
64 S3 {
66 bucket: String,
67 region: String,
68 endpoint: Option<String>,
69 },
70 Redis {
72 url: String,
74 #[serde(default)]
76 ttl_secs: Option<u64>,
77 },
78 Pg {
81 url: String,
83 max_conns: u32,
85 },
86 Tiered {
92 l1: Box<BackendConfig>,
94 l2: Box<BackendConfig>,
96 l3: Box<BackendConfig>,
98 #[serde(default)]
100 write_policy: WritePolicy,
101 },
102}
103
104impl Default for BackendConfig {
105 fn default() -> Self {
106 Self::Local {
107 path: PathBuf::from("/var/cache/sui"),
108 }
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn default_config_has_sane_values() {
118 let config = CacheConfig::default();
119 assert_eq!(config.listen, "0.0.0.0:5000");
120 assert_eq!(config.store_dir, "/nix/store");
121 assert_eq!(config.priority, 40);
122 assert!(config.want_mass_query);
123 assert!(config.signing_key.is_none());
124 }
125
126 #[test]
127 fn default_backend_is_local() {
128 let config = CacheConfig::default();
129 assert!(matches!(config.backend, BackendConfig::Local { .. }));
130 }
131
132 #[test]
133 fn config_serializes_to_json() {
134 let config = CacheConfig::default();
135 let json = serde_json::to_string(&config).unwrap();
136 assert!(json.contains("local"));
137 assert!(json.contains("5000"));
138 }
139
140 #[test]
141 fn config_roundtrips_through_json() {
142 let config = CacheConfig {
143 listen: "127.0.0.1:8080".to_string(),
144 backend: BackendConfig::S3 {
145 bucket: "my-cache".to_string(),
146 region: "us-east-1".to_string(),
147 endpoint: Some("http://localhost:9000".to_string()),
148 },
149 signing_key: Some(PathBuf::from("/tmp/key.sec")),
150 priority: 30,
151 want_mass_query: false,
152 store_dir: "/nix/store".to_string(),
153 require_sigs: true,
154 };
155 let json = serde_json::to_string_pretty(&config).unwrap();
156 let parsed: CacheConfig = serde_json::from_str(&json).unwrap();
157 assert_eq!(parsed.listen, "127.0.0.1:8080");
158 assert_eq!(parsed.priority, 30);
159 assert!(!parsed.want_mass_query);
160 assert!(parsed.require_sigs);
161 assert!(matches!(parsed.backend, BackendConfig::S3 { .. }));
162 }
163
164 #[test]
165 fn require_sigs_defaults_to_false_when_absent() {
166 let json = r#"{
169 "listen": "0.0.0.0:5000",
170 "backend": { "type": "local", "path": "/var/cache/sui" },
171 "signing_key": null,
172 "priority": 40,
173 "want_mass_query": true,
174 "store_dir": "/nix/store"
175 }"#;
176 let parsed: CacheConfig = serde_json::from_str(json).unwrap();
177 assert!(!parsed.require_sigs);
178 }
179
180 #[test]
181 fn tiered_backend_roundtrips_through_json() {
182 let backend = BackendConfig::Tiered {
183 l1: Box::new(BackendConfig::Redis {
184 url: "redis://redis:6379".to_string(),
185 ttl_secs: Some(3600),
186 }),
187 l2: Box::new(BackendConfig::Pg {
188 url: "postgres://pg:5432/sui".to_string(),
189 max_conns: 16,
190 }),
191 l3: Box::new(BackendConfig::S3 {
192 bucket: "sui-super-cache".to_string(),
193 region: "us-east-1".to_string(),
194 endpoint: None,
195 }),
196 write_policy: WritePolicy::WriteThrough,
197 };
198 let json = serde_json::to_string_pretty(&backend).unwrap();
199 assert!(json.contains("tiered"));
200 assert!(json.contains("redis"));
201 assert!(json.contains("write-through"));
202 let parsed: BackendConfig = serde_json::from_str(&json).unwrap();
203 match parsed {
204 BackendConfig::Tiered { l1, l2, l3, write_policy } => {
205 assert!(matches!(*l1, BackendConfig::Redis { .. }));
206 assert!(matches!(*l2, BackendConfig::Pg { .. }));
207 assert!(matches!(*l3, BackendConfig::S3 { .. }));
208 assert_eq!(write_policy, WritePolicy::WriteThrough);
209 }
210 other => panic!("expected tiered, got {other:?}"),
211 }
212 }
213
214 #[test]
215 fn tiered_write_policy_defaults_when_absent() {
216 let json = r#"{
218 "type": "tiered",
219 "l1": { "type": "redis", "url": "redis://r:6379" },
220 "l2": { "type": "pg", "url": "postgres://p:5432/s", "max_conns": 8 },
221 "l3": { "type": "local", "path": "/var/cache/sui" }
222 }"#;
223 let parsed: BackendConfig = serde_json::from_str(json).unwrap();
224 match parsed {
225 BackendConfig::Tiered { write_policy, .. } => {
226 assert_eq!(write_policy, WritePolicy::default());
227 assert_eq!(write_policy, WritePolicy::WriteThrough);
228 }
229 other => panic!("expected tiered, got {other:?}"),
230 }
231 }
232
233 #[test]
234 fn redis_ttl_defaults_to_none() {
235 let json = r#"{ "type": "redis", "url": "redis://r:6379" }"#;
236 let parsed: BackendConfig = serde_json::from_str(json).unwrap();
237 assert!(matches!(parsed, BackendConfig::Redis { ttl_secs: None, .. }));
238 }
239}