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}