cvx_core/config/
mod.rs

1//! Configuration types for ChronosVector.
2//!
3//! The primary configuration is [`CvxConfig`], which is deserialized from a TOML file.
4//! All fields have sensible defaults for development use.
5
6use std::path::PathBuf;
7
8use serde::{Deserialize, Serialize};
9
10/// Top-level ChronosVector configuration.
11///
12/// Deserialized from `config.toml`. All sections are optional with defaults.
13///
14/// # Example
15///
16/// ```
17/// use std::path::PathBuf;
18/// use cvx_core::CvxConfig;
19///
20/// let config: CvxConfig = toml::from_str(r#"
21///     [server]
22///     host = "0.0.0.0"
23///     port = 8080
24///
25///     [storage]
26///     data_dir = "./data"
27///
28///     [logging]
29///     level = "info"
30/// "#).unwrap();
31///
32/// assert_eq!(config.server.port, 8080);
33/// assert_eq!(config.storage.data_dir, PathBuf::from("./data"));
34/// ```
35#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
36#[serde(default)]
37pub struct CvxConfig {
38    /// Server network configuration.
39    pub server: ServerConfig,
40    /// Storage engine configuration.
41    pub storage: StorageConfig,
42    /// Index (ST-HNSW) configuration.
43    pub index: IndexConfig,
44    /// Analytics engine configuration.
45    pub analytics: AnalyticsConfig,
46    /// Logging and observability configuration.
47    pub logging: LoggingConfig,
48}
49
50impl CvxConfig {
51    /// Load configuration from a TOML file.
52    ///
53    /// Returns `CvxError::Config` if the file cannot be read or parsed.
54    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    /// Parse configuration from a TOML string.
64    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/// Network server configuration.
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
71#[serde(default)]
72pub struct ServerConfig {
73    /// Bind address for the HTTP server.
74    pub host: String,
75    /// HTTP port.
76    pub port: u16,
77    /// Optional gRPC port. If `None`, gRPC is disabled.
78    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/// Storage engine configuration.
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
93#[serde(default)]
94pub struct StorageConfig {
95    /// Root directory for all storage data.
96    pub data_dir: PathBuf,
97    /// Hot tier (RocksDB) configuration.
98    pub hot: HotTierConfig,
99    /// Warm tier (Parquet) configuration.
100    pub warm: WarmTierConfig,
101    /// Cold tier (object store) configuration.
102    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/// Hot tier (RocksDB) configuration.
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
118#[serde(default)]
119pub struct HotTierConfig {
120    /// Maximum size of the hot tier in megabytes.
121    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/// Warm tier (Parquet) configuration.
131#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
132#[serde(default)]
133pub struct WarmTierConfig {
134    /// Whether the warm tier is enabled.
135    pub enabled: bool,
136}
137
138/// Cold tier (object store) configuration.
139#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
140#[serde(default)]
141pub struct ColdTierConfig {
142    /// Whether the cold tier is enabled.
143    pub enabled: bool,
144    /// Object store endpoint (e.g., `s3://bucket/prefix`).
145    pub endpoint: Option<String>,
146}
147
148/// ST-HNSW index configuration.
149#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
150#[serde(default)]
151pub struct IndexConfig {
152    /// Maximum connections per node per layer.
153    pub m: usize,
154    /// Search width during index construction.
155    pub ef_construction: usize,
156    /// Search width during queries.
157    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/// Analytics engine configuration.
171#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
172#[serde(default)]
173pub struct AnalyticsConfig {
174    /// Enable Neural ODE prediction (requires `torch-backend` feature).
175    pub neural_ode: bool,
176    /// Path to TorchScript Neural ODE model file (`.pt`).
177    /// Required when `neural_ode = true` and `torch-backend` feature is enabled.
178    pub model_path: Option<PathBuf>,
179    /// Change point detection method.
180    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/// Logging and observability configuration.
194#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
195#[serde(default)]
196pub struct LoggingConfig {
197    /// Log level (`trace`, `debug`, `info`, `warn`, `error`).
198    pub level: String,
199    /// Log format (`text` or `json`).
200    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        // Rest should be defaults
245        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}