Skip to main content

ralph_workflow/config/unified/
loading.rs

1//! Configuration loading and initialization.
2//!
3//! This module provides functions for loading and initializing Ralph's unified configuration.
4//!
5//! # Loading Strategy
6//!
7//! Configuration loading supports both production and testing scenarios:
8//!
9//! - **Production**: Uses `load_default()` which reads from `~/.config/ralph-workflow.toml`
10//! - **Testing**: Uses `load_with_env()` with a `ConfigEnvironment` trait for test isolation
11//!
12//! # Initialization
13//!
14//! Ralph can automatically create a default configuration file if none exists:
15//!
16//! ```rust
17//! use ralph_workflow::config::unified::UnifiedConfig;
18//!
19//! // Ensure config exists, creating it if needed
20//! let result = UnifiedConfig::ensure_config_exists()?;
21//!
22//! // Load the config
23//! let config = UnifiedConfig::load_default()
24//!     .expect("Config should exist after ensure_config_exists");
25//! # Ok::<(), std::io::Error>(())
26//! ```
27
28use super::types::UnifiedConfig;
29
30/// Result of config initialization.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum ConfigInitResult {
33    /// Config was created successfully.
34    Created,
35    /// Config already exists.
36    AlreadyExists,
37}
38
39/// Error type for unified config loading.
40#[derive(Debug, thiserror::Error)]
41pub enum ConfigLoadError {
42    #[error("Failed to read config file: {0}")]
43    Io(#[from] std::io::Error),
44    #[error("Failed to parse TOML: {0}")]
45    Toml(#[from] toml::de::Error),
46}
47
48/// Default unified config template embedded at compile time.
49pub const DEFAULT_UNIFIED_CONFIG: &str = include_str!("../../../examples/ralph-workflow.toml");
50
51impl UnifiedConfig {
52    /// Load unified configuration from the default path.
53    ///
54    /// Returns None if the file doesn't exist.
55    ///
56    /// # Examples
57    ///
58    /// ```rust
59    /// use ralph_workflow::config::unified::UnifiedConfig;
60    ///
61    /// if let Some(config) = UnifiedConfig::load_default() {
62    ///     println!("Verbosity level: {}", config.general.verbosity);
63    /// }
64    /// ```
65    #[must_use]
66    pub fn load_default() -> Option<Self> {
67        Self::load_with_env(&super::super::path_resolver::RealConfigEnvironment)
68    }
69
70    /// Load unified configuration using a `ConfigEnvironment`.
71    ///
72    /// This is the testable version of `load_default`. It reads from the
73    /// unified config path as determined by the environment.
74    ///
75    /// Returns None if no config path is available or the file doesn't exist.
76    pub fn load_with_env(env: &dyn super::super::path_resolver::ConfigEnvironment) -> Option<Self> {
77        env.unified_config_path().and_then(|path| {
78            if env.file_exists(&path) {
79                Self::load_from_path_with_env(&path, env).ok()
80            } else {
81                None
82            }
83        })
84    }
85
86    /// Load unified configuration from a specific path using a `ConfigEnvironment`.
87    ///
88    /// This is the testable version of `load_from_path`.
89    ///
90    /// # Errors
91    ///
92    /// Returns error if the operation fails.
93    pub fn load_from_path_with_env(
94        path: &std::path::Path,
95        env: &dyn super::super::path_resolver::ConfigEnvironment,
96    ) -> Result<Self, ConfigLoadError> {
97        let contents = env.read_file(path)?;
98        let config: Self = toml::from_str(&contents)?;
99        Ok(config)
100    }
101
102    /// Load unified configuration from pre-read content.
103    ///
104    /// This avoids re-reading the file when content is already available.
105    /// The path is used only for error messages.
106    ///
107    /// # Arguments
108    ///
109    /// * `content` - The raw TOML content string
110    ///
111    /// # Errors
112    ///
113    /// Returns an error if the TOML syntax is invalid or required fields are missing.
114    ///
115    /// # Examples
116    ///
117    /// ```rust
118    /// use ralph_workflow::config::unified::UnifiedConfig;
119    ///
120    /// let toml_content = r#"
121    ///     [general]
122    ///     verbosity = 3
123    /// "#;
124    ///
125    /// let config = UnifiedConfig::load_from_content(toml_content)?;
126    /// assert_eq!(config.general.verbosity, 3);
127    /// # Ok::<(), Box<dyn std::error::Error>>(())
128    /// ```
129    pub fn load_from_content(content: &str) -> Result<Self, ConfigLoadError> {
130        let config: Self = toml::from_str(content)?;
131        Ok(config)
132    }
133
134    /// Ensure unified config file exists, creating it from template if needed.
135    ///
136    /// This creates `~/.config/ralph-workflow.toml` with the default template
137    /// if it doesn't already exist.
138    ///
139    /// # Returns
140    ///
141    /// - `Created` if the config file was created
142    /// - `AlreadyExists` if the config file already existed
143    ///
144    /// # Errors
145    ///
146    /// Returns an error if:
147    /// - The home directory cannot be determined
148    /// - The config file cannot be written
149    ///
150    /// # Examples
151    ///
152    /// ```rust
153    /// use ralph_workflow::config::unified::{UnifiedConfig, ConfigInitResult};
154    ///
155    /// match UnifiedConfig::ensure_config_exists() {
156    ///     Ok(ConfigInitResult::Created) => println!("Created new config"),
157    ///     Ok(ConfigInitResult::AlreadyExists) => println!("Config already exists"),
158    ///     Err(e) => eprintln!("Failed to create config: {}", e),
159    /// }
160    /// # Ok::<(), std::io::Error>(())
161    /// ```
162    pub fn ensure_config_exists() -> std::io::Result<ConfigInitResult> {
163        Self::ensure_config_exists_with_env(&super::super::path_resolver::RealConfigEnvironment)
164    }
165
166    /// Ensure unified config file exists using a `ConfigEnvironment`.
167    ///
168    /// This is the testable version of `ensure_config_exists`.
169    ///
170    /// # Errors
171    ///
172    /// Returns error if the operation fails.
173    pub fn ensure_config_exists_with_env(
174        env: &dyn super::super::path_resolver::ConfigEnvironment,
175    ) -> std::io::Result<ConfigInitResult> {
176        let Some(path) = env.unified_config_path() else {
177            return Err(std::io::Error::new(
178                std::io::ErrorKind::NotFound,
179                "Cannot determine config directory (no home directory)",
180            ));
181        };
182
183        Self::ensure_config_exists_at_with_env(&path, env)
184    }
185
186    /// Ensure a config file exists at the specified path.
187    ///
188    /// This is useful for custom config file locations or testing.
189    ///
190    /// # Errors
191    ///
192    /// Returns error if the operation fails.
193    pub fn ensure_config_exists_at(path: &std::path::Path) -> std::io::Result<ConfigInitResult> {
194        Self::ensure_config_exists_at_with_env(
195            path,
196            &super::super::path_resolver::RealConfigEnvironment,
197        )
198    }
199
200    /// Ensure a config file exists at the specified path using a `ConfigEnvironment`.
201    ///
202    /// This is the testable version of `ensure_config_exists_at`.
203    ///
204    /// # Errors
205    ///
206    /// Returns error if the operation fails.
207    pub fn ensure_config_exists_at_with_env(
208        path: &std::path::Path,
209        env: &dyn super::super::path_resolver::ConfigEnvironment,
210    ) -> std::io::Result<ConfigInitResult> {
211        if env.file_exists(path) {
212            return Ok(ConfigInitResult::AlreadyExists);
213        }
214
215        // Write the default template (write_file creates parent directories)
216        env.write_file(path, DEFAULT_UNIFIED_CONFIG)?;
217
218        Ok(ConfigInitResult::Created)
219    }
220}