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}