type_bridge_server/
config.rs1use 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 #[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 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()); }
225
226 #[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 #[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 #[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); }
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"); 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"); assert_eq!(audit.file_path, ""); }
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 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}