fraiseql_server/config/loader.rs
1//! Configuration loading from TOML files and environment variables.
2//!
3//! Loading is performed via methods on [`RuntimeConfig`] that locate and
4//! deserialize `fraiseql.toml`, then validate the result with
5//! `ConfigValidator` in the `validation` sub-module.
6
7use std::{env, path::Path};
8
9use fraiseql_error::ConfigError;
10
11use crate::config::{RuntimeConfig, validation::ConfigValidator};
12
13impl RuntimeConfig {
14 /// Load configuration from file with full validation
15 ///
16 /// # Errors
17 ///
18 /// Returns `ConfigError::ReadError` if the file cannot be read.
19 /// Returns `ConfigError::ParseError` if the file content is not valid TOML.
20 /// Returns `ConfigError` if validation fails or required environment variables are missing.
21 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
22 let path = path.as_ref();
23
24 let content = std::fs::read_to_string(path).map_err(|e| ConfigError::ReadError {
25 path: path.to_path_buf(),
26 source: e,
27 })?;
28
29 let config: RuntimeConfig =
30 toml::from_str(&content).map_err(|e| ConfigError::ParseError { source: e })?;
31
32 // Run comprehensive validation
33 let validation = ConfigValidator::new(&config).validate();
34 let warnings = validation.into_result()?;
35
36 // Log warnings
37 for warning in warnings {
38 tracing::warn!("Configuration warning: {}", warning);
39 }
40
41 Ok(config)
42 }
43
44 /// Load configuration from default locations
45 ///
46 /// # Errors
47 ///
48 /// Returns `ConfigError::NotFound` if no configuration file is found in any default location.
49 /// Returns `ConfigError` if the found file cannot be read, parsed, or fails validation.
50 pub fn load() -> Result<Self, ConfigError> {
51 // Check FRAISEQL_CONFIG environment variable
52 if let Ok(path) = env::var("FRAISEQL_CONFIG") {
53 return Self::from_file(&path);
54 }
55
56 // Check current directory
57 let local_config = Path::new("./fraiseql.toml");
58 if local_config.exists() {
59 return Self::from_file(local_config);
60 }
61
62 // Check user config directory
63 if let Some(config_dir) = dirs::config_dir() {
64 let user_config = config_dir.join("fraiseql/config.toml");
65 if user_config.exists() {
66 return Self::from_file(&user_config);
67 }
68 }
69
70 Err(ConfigError::NotFound)
71 }
72
73 /// Load configuration with optional file path (CLI argument)
74 ///
75 /// # Errors
76 ///
77 /// Returns `ConfigError` if loading from the given path or from default locations fails.
78 pub fn load_with_path(path: Option<&Path>) -> Result<Self, ConfigError> {
79 match path {
80 Some(p) => Self::from_file(p),
81 None => Self::load(),
82 }
83 }
84
85 /// Validate configuration without loading env vars (for dry-run/testing)
86 ///
87 /// # Errors
88 ///
89 /// Returns `ConfigError::ParseError` if the content is not valid TOML or cannot be
90 /// deserialized.
91 pub fn validate_syntax(content: &str) -> Result<(), ConfigError> {
92 let _config: RuntimeConfig =
93 toml::from_str(content).map_err(|e| ConfigError::ParseError { source: e })?;
94 Ok(())
95 }
96}