Skip to main content

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}