1use std::path::PathBuf;
7
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
36#[serde(default)]
37pub struct CvxConfig {
38 pub server: ServerConfig,
40 pub storage: StorageConfig,
42 pub index: IndexConfig,
44 pub analytics: AnalyticsConfig,
46 pub logging: LoggingConfig,
48}
49
50impl CvxConfig {
51 pub fn from_file(path: &std::path::Path) -> Result<Self, crate::CvxError> {
55 let content = std::fs::read_to_string(path).map_err(|e| {
56 crate::CvxError::Config(format!("cannot read config file {}: {e}", path.display()))
57 })?;
58 toml::from_str(&content).map_err(|e| {
59 crate::CvxError::Config(format!("cannot parse config file {}: {e}", path.display()))
60 })
61 }
62
63 pub fn parse(s: &str) -> Result<Self, crate::CvxError> {
65 toml::from_str(s).map_err(|e| crate::CvxError::Config(format!("cannot parse config: {e}")))
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
71#[serde(default)]
72pub struct ServerConfig {
73 pub host: String,
75 pub port: u16,
77 pub grpc_port: Option<u16>,
79}
80
81impl Default for ServerConfig {
82 fn default() -> Self {
83 Self {
84 host: "0.0.0.0".into(),
85 port: 8080,
86 grpc_port: None,
87 }
88 }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
93#[serde(default)]
94pub struct StorageConfig {
95 pub data_dir: PathBuf,
97 pub hot: HotTierConfig,
99 pub warm: WarmTierConfig,
101 pub cold: ColdTierConfig,
103}
104
105impl Default for StorageConfig {
106 fn default() -> Self {
107 Self {
108 data_dir: PathBuf::from("./data"),
109 hot: HotTierConfig::default(),
110 warm: WarmTierConfig::default(),
111 cold: ColdTierConfig::default(),
112 }
113 }
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
118#[serde(default)]
119pub struct HotTierConfig {
120 pub max_size_mb: u64,
122}
123
124impl Default for HotTierConfig {
125 fn default() -> Self {
126 Self { max_size_mb: 1024 }
127 }
128}
129
130#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
132#[serde(default)]
133pub struct WarmTierConfig {
134 pub enabled: bool,
136}
137
138#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
140#[serde(default)]
141pub struct ColdTierConfig {
142 pub enabled: bool,
144 pub endpoint: Option<String>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
150#[serde(default)]
151pub struct IndexConfig {
152 pub m: usize,
154 pub ef_construction: usize,
156 pub ef_search: usize,
158}
159
160impl Default for IndexConfig {
161 fn default() -> Self {
162 Self {
163 m: 16,
164 ef_construction: 200,
165 ef_search: 50,
166 }
167 }
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
172#[serde(default)]
173pub struct AnalyticsConfig {
174 pub neural_ode: bool,
176 pub model_path: Option<PathBuf>,
179 pub change_detection: String,
181}
182
183impl Default for AnalyticsConfig {
184 fn default() -> Self {
185 Self {
186 neural_ode: false,
187 model_path: None,
188 change_detection: "pelt".into(),
189 }
190 }
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
195#[serde(default)]
196pub struct LoggingConfig {
197 pub level: String,
199 pub format: String,
201}
202
203impl Default for LoggingConfig {
204 fn default() -> Self {
205 Self {
206 level: "info".into(),
207 format: "text".into(),
208 }
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn default_config_is_valid() {
218 let config = CvxConfig::default();
219 assert_eq!(config.server.port, 8080);
220 assert_eq!(config.index.m, 16);
221 assert_eq!(config.logging.level, "info");
222 }
223
224 #[test]
225 fn deserialize_minimal_toml() {
226 let config: CvxConfig = toml::from_str("").unwrap();
227 assert_eq!(config, CvxConfig::default());
228 }
229
230 #[test]
231 fn deserialize_partial_toml() {
232 let config: CvxConfig = toml::from_str(
233 r#"
234 [server]
235 port = 9090
236
237 [index]
238 m = 32
239 "#,
240 )
241 .unwrap();
242 assert_eq!(config.server.port, 9090);
243 assert_eq!(config.index.m, 32);
244 assert_eq!(config.storage.data_dir, PathBuf::from("./data"));
246 }
247
248 #[test]
249 fn deserialize_full_config_example() {
250 let toml_str = r#"
251 [server]
252 host = "0.0.0.0"
253 port = 8080
254
255 [storage]
256 data_dir = "./data"
257
258 [storage.hot]
259 max_size_mb = 2048
260
261 [storage.warm]
262 enabled = true
263
264 [storage.cold]
265 enabled = true
266 endpoint = "s3://my-bucket/cvx"
267
268 [index]
269 m = 16
270 ef_construction = 200
271 ef_search = 50
272
273 [analytics]
274 neural_ode = false
275 change_detection = "pelt"
276
277 [logging]
278 level = "info"
279 format = "json"
280 "#;
281 let config: CvxConfig = toml::from_str(toml_str).unwrap();
282 assert_eq!(config.storage.hot.max_size_mb, 2048);
283 assert!(config.storage.warm.enabled);
284 assert!(config.storage.cold.enabled);
285 assert_eq!(
286 config.storage.cold.endpoint.as_deref(),
287 Some("s3://my-bucket/cvx")
288 );
289 assert_eq!(config.logging.format, "json");
290 }
291
292 #[test]
293 fn from_str_returns_error_on_invalid_toml() {
294 let result = CvxConfig::parse("[invalid toml %%");
295 assert!(result.is_err());
296 let err = result.unwrap_err();
297 assert!(matches!(err, crate::CvxError::Config(_)));
298 }
299
300 #[test]
301 fn config_serialization_roundtrip() {
302 let config = CvxConfig::default();
303 let toml_str = toml::to_string_pretty(&config).unwrap();
304 let recovered: CvxConfig = toml::from_str(&toml_str).unwrap();
305 assert_eq!(config, recovered);
306 }
307}