Skip to main content

type_bridge_server/
config.rs

1use serde::Deserialize;
2
3#[derive(Debug, Deserialize)]
4pub struct ServerConfig {
5    pub server: ServerSection,
6    pub typedb: TypeDBSection,
7    #[serde(default)]
8    pub schema: SchemaSection,
9    #[serde(default)]
10    pub interceptors: InterceptorsSection,
11    #[serde(default)]
12    pub logging: LoggingSection,
13}
14
15#[derive(Debug, Deserialize)]
16pub struct ServerSection {
17    #[serde(default = "default_host")]
18    pub host: String,
19    #[serde(default = "default_port")]
20    pub port: u16,
21}
22
23#[derive(Debug, Deserialize)]
24pub struct TypeDBSection {
25    pub address: String,
26    pub database: String,
27    #[serde(default = "default_username")]
28    pub username: String,
29    #[serde(default = "default_password")]
30    pub password: String,
31}
32
33#[derive(Debug, Default, Deserialize)]
34pub struct SchemaSection {
35    #[serde(default)]
36    pub source_file: String,
37}
38
39#[derive(Debug, Default, Deserialize)]
40pub struct InterceptorsSection {
41    #[serde(default)]
42    pub enabled: Vec<String>,
43    #[serde(default, rename = "audit-log")]
44    pub audit_log: Option<AuditLogConfig>,
45}
46
47#[derive(Debug, Clone, Deserialize)]
48pub struct AuditLogConfig {
49    #[serde(default = "default_audit_output")]
50    pub output: String,
51    #[serde(default)]
52    pub file_path: String,
53}
54
55#[derive(Debug, Deserialize)]
56pub struct LoggingSection {
57    #[serde(default = "default_log_level")]
58    pub level: String,
59    #[serde(default = "default_log_format")]
60    pub format: String,
61}
62
63impl Default for LoggingSection {
64    fn default() -> Self {
65        Self {
66            level: default_log_level(),
67            format: default_log_format(),
68        }
69    }
70}
71
72fn default_host() -> String {
73    "0.0.0.0".to_string()
74}
75
76fn default_port() -> u16 {
77    8080
78}
79
80fn default_username() -> String {
81    "admin".to_string()
82}
83
84fn default_password() -> String {
85    "password".to_string()
86}
87
88fn default_log_level() -> String {
89    "info".to_string()
90}
91
92fn default_log_format() -> String {
93    "json".to_string()
94}
95
96fn default_audit_output() -> String {
97    "stdout".to_string()
98}
99
100impl ServerConfig {
101    pub fn from_file(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
102        let content = std::fs::read_to_string(path)?;
103        let config: ServerConfig = toml::from_str(&content)?;
104        Ok(config)
105    }
106}
107
108#[cfg(test)]
109#[cfg_attr(coverage_nightly, coverage(off))]
110mod tests {
111    use super::*;
112
113    const FULL_CONFIG: &str = r#"
114[server]
115host = "127.0.0.1"
116port = 9090
117
118[typedb]
119address = "localhost:1729"
120database = "mydb"
121username = "root"
122password = "secret"
123
124[schema]
125source_file = "schema.tql"
126
127[interceptors]
128enabled = ["audit-log"]
129
130[interceptors.audit-log]
131output = "file"
132file_path = "/tmp/audit.log"
133
134[logging]
135level = "debug"
136format = "text"
137"#;
138
139    const MINIMAL_CONFIG: &str = r#"
140[server]
141
142[typedb]
143address = "localhost:1729"
144database = "mydb"
145"#;
146
147    // --- from_file tests ---
148
149    #[test]
150    fn from_file_valid_full_config() {
151        let dir = tempfile::tempdir().unwrap();
152        let path = dir.path().join("server.toml");
153        std::fs::write(&path, FULL_CONFIG).unwrap();
154
155        let config = ServerConfig::from_file(path.to_str().unwrap()).unwrap();
156        assert_eq!(config.server.host, "127.0.0.1");
157        assert_eq!(config.server.port, 9090);
158        assert_eq!(config.typedb.address, "localhost:1729");
159        assert_eq!(config.typedb.database, "mydb");
160        assert_eq!(config.typedb.username, "root");
161        assert_eq!(config.typedb.password, "secret");
162        assert_eq!(config.schema.source_file, "schema.tql");
163        assert_eq!(config.interceptors.enabled, vec!["audit-log"]);
164        let audit = config.interceptors.audit_log.unwrap();
165        assert_eq!(audit.output, "file");
166        assert_eq!(audit.file_path, "/tmp/audit.log");
167        assert_eq!(config.logging.level, "debug");
168        assert_eq!(config.logging.format, "text");
169    }
170
171    #[test]
172    fn from_file_valid_minimal_config() {
173        let dir = tempfile::tempdir().unwrap();
174        let path = dir.path().join("server.toml");
175        std::fs::write(&path, MINIMAL_CONFIG).unwrap();
176
177        let config = ServerConfig::from_file(path.to_str().unwrap()).unwrap();
178        // Defaults should kick in
179        assert_eq!(config.server.host, "0.0.0.0");
180        assert_eq!(config.server.port, 8080);
181        assert_eq!(config.typedb.username, "admin");
182        assert_eq!(config.typedb.password, "password");
183        assert_eq!(config.schema.source_file, "");
184        assert!(config.interceptors.enabled.is_empty());
185        assert!(config.interceptors.audit_log.is_none());
186        assert_eq!(config.logging.level, "info");
187        assert_eq!(config.logging.format, "json");
188    }
189
190    #[test]
191    fn from_file_missing_file() {
192        let result = ServerConfig::from_file("/nonexistent/path/server.toml");
193        assert!(result.is_err());
194    }
195
196    #[test]
197    fn from_file_invalid_toml() {
198        let dir = tempfile::tempdir().unwrap();
199        let path = dir.path().join("bad.toml");
200        std::fs::write(&path, "this is not valid toml {{{}}}").unwrap();
201
202        let result = ServerConfig::from_file(path.to_str().unwrap());
203        assert!(result.is_err());
204    }
205
206    #[test]
207    fn from_file_missing_required_typedb_section() {
208        let dir = tempfile::tempdir().unwrap();
209        let path = dir.path().join("incomplete.toml");
210        std::fs::write(&path, "[server]\n").unwrap();
211
212        let result = ServerConfig::from_file(path.to_str().unwrap());
213        assert!(result.is_err());
214    }
215
216    #[test]
217    fn from_file_missing_required_typedb_fields() {
218        let dir = tempfile::tempdir().unwrap();
219        let path = dir.path().join("incomplete.toml");
220        std::fs::write(&path, "[server]\n[typedb]\n").unwrap();
221
222        let result = ServerConfig::from_file(path.to_str().unwrap());
223        assert!(result.is_err()); // address and database are required
224    }
225
226    // --- Default function tests ---
227
228    #[test]
229    fn default_host_value() {
230        assert_eq!(default_host(), "0.0.0.0");
231    }
232
233    #[test]
234    fn default_port_value() {
235        assert_eq!(default_port(), 8080);
236    }
237
238    #[test]
239    fn default_username_value() {
240        assert_eq!(default_username(), "admin");
241    }
242
243    #[test]
244    fn default_password_value() {
245        assert_eq!(default_password(), "password");
246    }
247
248    #[test]
249    fn default_log_level_value() {
250        assert_eq!(default_log_level(), "info");
251    }
252
253    #[test]
254    fn default_log_format_value() {
255        assert_eq!(default_log_format(), "json");
256    }
257
258    #[test]
259    fn default_audit_output_value() {
260        assert_eq!(default_audit_output(), "stdout");
261    }
262
263    // --- LoggingSection default ---
264
265    #[test]
266    fn logging_section_default() {
267        let logging = LoggingSection::default();
268        assert_eq!(logging.level, "info");
269        assert_eq!(logging.format, "json");
270    }
271
272    // --- Serde deserialization edge cases ---
273
274    #[test]
275    fn server_section_custom_host_default_port() {
276        let toml = r#"
277[server]
278host = "192.168.1.1"
279
280[typedb]
281address = "localhost:1729"
282database = "db"
283"#;
284        let config: ServerConfig = toml::from_str(toml).unwrap();
285        assert_eq!(config.server.host, "192.168.1.1");
286        assert_eq!(config.server.port, 8080); // default
287    }
288
289    #[test]
290    fn server_section_custom_port_default_host() {
291        let toml = r#"
292[server]
293port = 3000
294
295[typedb]
296address = "localhost:1729"
297database = "db"
298"#;
299        let config: ServerConfig = toml::from_str(toml).unwrap();
300        assert_eq!(config.server.host, "0.0.0.0"); // default
301        assert_eq!(config.server.port, 3000);
302    }
303
304    #[test]
305    fn typedb_section_custom_credentials() {
306        let toml = r#"
307[server]
308
309[typedb]
310address = "remote:1729"
311database = "prod"
312username = "superuser"
313password = "hunter2"
314"#;
315        let config: ServerConfig = toml::from_str(toml).unwrap();
316        assert_eq!(config.typedb.username, "superuser");
317        assert_eq!(config.typedb.password, "hunter2");
318    }
319
320    #[test]
321    fn schema_section_default_when_missing() {
322        let config: ServerConfig = toml::from_str(MINIMAL_CONFIG).unwrap();
323        assert_eq!(config.schema.source_file, "");
324    }
325
326    #[test]
327    fn schema_section_with_file() {
328        let toml = r#"
329[server]
330[typedb]
331address = "localhost:1729"
332database = "db"
333[schema]
334source_file = "my_schema.tql"
335"#;
336        let config: ServerConfig = toml::from_str(toml).unwrap();
337        assert_eq!(config.schema.source_file, "my_schema.tql");
338    }
339
340    #[test]
341    fn interceptors_enabled_empty_by_default() {
342        let config: ServerConfig = toml::from_str(MINIMAL_CONFIG).unwrap();
343        assert!(config.interceptors.enabled.is_empty());
344        assert!(config.interceptors.audit_log.is_none());
345    }
346
347    #[test]
348    fn interceptors_enabled_without_audit_config() {
349        let toml = r#"
350[server]
351[typedb]
352address = "localhost:1729"
353database = "db"
354[interceptors]
355enabled = ["audit-log"]
356"#;
357        let config: ServerConfig = toml::from_str(toml).unwrap();
358        assert_eq!(config.interceptors.enabled, vec!["audit-log"]);
359        assert!(config.interceptors.audit_log.is_none());
360    }
361
362    #[test]
363    fn interceptors_with_audit_config() {
364        let toml = r#"
365[server]
366[typedb]
367address = "localhost:1729"
368database = "db"
369[interceptors]
370enabled = ["audit-log"]
371[interceptors.audit-log]
372output = "file"
373file_path = "/var/log/audit.jsonl"
374"#;
375        let config: ServerConfig = toml::from_str(toml).unwrap();
376        let audit = config.interceptors.audit_log.unwrap();
377        assert_eq!(audit.output, "file");
378        assert_eq!(audit.file_path, "/var/log/audit.jsonl");
379    }
380
381    #[test]
382    fn audit_log_config_defaults() {
383        let toml = r#"
384[server]
385[typedb]
386address = "localhost:1729"
387database = "db"
388[interceptors]
389enabled = ["audit-log"]
390[interceptors.audit-log]
391"#;
392        let config: ServerConfig = toml::from_str(toml).unwrap();
393        let audit = config.interceptors.audit_log.unwrap();
394        assert_eq!(audit.output, "stdout"); // default
395        assert_eq!(audit.file_path, ""); // default
396    }
397
398    #[test]
399    fn extra_fields_ignored() {
400        let toml = r#"
401[server]
402host = "0.0.0.0"
403unknown_field = "ignored"
404
405[typedb]
406address = "localhost:1729"
407database = "db"
408"#;
409        // toml crate with serde ignores unknown fields by default
410        let result: Result<ServerConfig, _> = toml::from_str(toml);
411        assert!(result.is_ok());
412    }
413
414    #[test]
415    fn multiple_interceptors_enabled() {
416        let toml = r#"
417[server]
418[typedb]
419address = "localhost:1729"
420database = "db"
421[interceptors]
422enabled = ["audit-log", "rate-limiter", "custom"]
423"#;
424        let config: ServerConfig = toml::from_str(toml).unwrap();
425        assert_eq!(config.interceptors.enabled.len(), 3);
426    }
427}