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