agpm_cli/config/
parser.rs

1//! Generic configuration parsing utilities.
2//!
3//! This module provides generic TOML parsing functionality that can be used
4//! with any configuration structure that implements the appropriate serde traits.
5//! It includes enhanced error reporting with file path context.
6//!
7//! # Features
8//!
9//! - **Generic Parsing**: Works with any `DeserializeOwned` type
10//! - **Rich Error Context**: Includes file paths in error messages
11//! - **TOML Focus**: Specifically designed for TOML configuration files
12//! - **Path Safety**: Handles file system errors gracefully
13//!
14//! # Design Philosophy
15//!
16//! The parser is designed to be:
17//! - **Simple**: Minimal API surface with clear semantics
18//! - **Generic**: Reusable across different configuration types
19//! - **Informative**: Provides clear error messages for debugging
20//! - **Safe**: Handles file system and parsing errors appropriately
21//!
22//! # Usage Patterns
23//!
24//! ## Direct Parsing
25//!
26//! ```rust,no_run
27//! use agpm_cli::config::parse_config;
28//! use serde::Deserialize;
29//! use std::path::Path;
30//!
31//! #[derive(Deserialize)]
32//! struct MyConfig {
33//!     name: String,
34//!     version: String,
35//! }
36//!
37//! # fn example() -> anyhow::Result<()> {
38//! let config: MyConfig = parse_config(Path::new("config.toml"))?;
39//! println!("Config: {} v{}", config.name, config.version);
40//! # Ok(())
41//! # }
42//! ```
43//!
44//! ## With Complex Types
45//!
46//! ```rust,no_run
47//! use agpm_cli::config::{parse_config, AgentManifest, SnippetManifest};
48//! use std::path::Path;
49//!
50//! # fn example() -> anyhow::Result<()> {
51//! // Parse agent configuration
52//! let agent: AgentManifest = parse_config(Path::new("agent.toml"))?;
53//!
54//! // Parse snippet configuration  
55//! let snippet: SnippetManifest = parse_config(Path::new("snippet.toml"))?;
56//! # Ok(())
57//! # }
58//! ```
59//!
60//! # Error Handling
61//!
62//! The parser provides detailed error messages that include:
63//!
64//! - **File Context**: Which file failed to parse
65//! - **Operation Context**: Whether it was a read or parse failure
66//! - **Underlying Error**: The specific I/O or TOML parsing error
67//!
68//! Example error output:
69//! ```text
70//! Failed to parse config file: /path/to/config.toml
71//! Caused by:
72//!     invalid TOML value, expected string
73//! ```
74//!
75//! # Integration
76//!
77//! This parser is used throughout AGPM for:
78//!
79//! - Agent manifest loading ([`AgentManifest::load`])
80//! - Snippet manifest loading ([`SnippetManifest::load`])
81//! - Generic configuration file parsing
82//! - Test fixtures and development tools
83//!
84//! [`AgentManifest::load`]: crate::config::AgentManifest::load
85//! [`SnippetManifest::load`]: crate::config::SnippetManifest::load
86
87use anyhow::{Context, Result};
88use std::path::Path;
89
90/// Parse a TOML configuration file into the specified type.
91///
92/// Generic function that reads a TOML file and deserializes it into any type
93/// that implements [`serde::de::DeserializeOwned`]. Provides enhanced error
94/// messages that include the file path context.
95///
96/// # Type Parameters
97///
98/// - `T`: The target type that implements `DeserializeOwned`
99///
100/// # Parameters
101///
102/// - `path`: Path to the TOML configuration file to parse
103///
104/// # Returns
105///
106/// The parsed configuration object of type `T`.
107///
108/// # Examples
109///
110/// ## Basic Usage
111///
112/// ```rust,no_run
113/// use agpm_cli::config::parse_config;
114/// use serde::Deserialize;
115/// use std::path::Path;
116///
117/// #[derive(Deserialize)]
118/// struct Config {
119///     name: String,
120///     port: u16,
121/// }
122///
123/// # fn example() -> anyhow::Result<()> {
124/// let config: Config = parse_config(Path::new("server.toml"))?;
125/// println!("Starting {} on port {}", config.name, config.port);
126/// # Ok(())
127/// # }
128/// ```
129///
130/// ## With AGPM Types
131///
132/// ```rust,no_run
133/// use agpm_cli::config::{parse_config, AgentManifest};
134/// use std::path::Path;
135///
136/// # fn example() -> anyhow::Result<()> {
137/// let manifest: AgentManifest = parse_config(Path::new("my-agent.toml"))?;
138/// println!("Loaded agent: {}", manifest.metadata.name);
139/// # Ok(())
140/// # }
141/// ```
142///
143/// ## Error Handling
144///
145/// ```rust,no_run
146/// use agpm_cli::config::parse_config;
147/// use serde::Deserialize;
148/// use std::path::Path;
149///
150/// #[derive(Deserialize)]
151/// struct Config { name: String }
152///
153/// # fn example() {
154/// match parse_config::<Config>(Path::new("missing.toml")) {
155///     Ok(config) => println!("Config loaded: {}", config.name),
156///     Err(e) => eprintln!("Failed to load config: {}", e),
157/// }
158/// # }
159/// ```
160///
161/// # Error Conditions
162///
163/// This function returns an error if:
164///
165/// ## File System Errors
166/// - File does not exist
167/// - Insufficient permissions to read the file
168/// - I/O errors during file reading
169/// - Path is a directory, not a file
170///
171/// ## Parsing Errors
172/// - File contains invalid TOML syntax
173/// - TOML structure doesn't match the target type `T`
174/// - Required fields are missing
175/// - Field types don't match expectations
176/// - TOML contains unsupported features for the target type
177///
178/// # Error Messages
179///
180/// The function provides two levels of error context:
181///
182/// 1. **File Operation Context**: "Failed to read config file: /path/to/file.toml"
183/// 2. **Parsing Context**: "Failed to parse config file: /path/to/file.toml"
184///
185/// The underlying error (file system or TOML parsing) is preserved as the cause.
186///
187/// # Performance
188///
189/// - Reads the entire file into memory before parsing
190/// - TOML parsing is generally fast for typical configuration file sizes
191/// - No caching - each call performs a fresh read and parse
192///
193/// # Thread Safety
194///
195/// This function is thread-safe and can be called concurrently from multiple threads.
196/// Each call operates independently on the file system.
197pub fn parse_config<T>(path: &Path) -> Result<T>
198where
199    T: serde::de::DeserializeOwned,
200{
201    let content = std::fs::read_to_string(path)
202        .with_context(|| format!("Failed to read config file: {}", path.display()))?;
203
204    let config: T = toml::from_str(&content)
205        .with_context(|| format!("Failed to parse config file: {}", path.display()))?;
206
207    Ok(config)
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_parse_config() {
216        use tempfile::tempdir;
217
218        let temp = tempdir().unwrap();
219        let config_path = temp.path().join("test.toml");
220
221        #[derive(serde::Deserialize)]
222        struct TestConfig {
223            name: String,
224            value: i32,
225        }
226
227        let toml_content = r#"
228            name = "test"
229            value = 42
230        "#;
231
232        std::fs::write(&config_path, toml_content).unwrap();
233
234        let config: TestConfig = parse_config(&config_path).unwrap();
235        assert_eq!(config.name, "test");
236        assert_eq!(config.value, 42);
237    }
238
239    #[test]
240    fn test_parse_config_error() {
241        use tempfile::tempdir;
242
243        let temp = tempdir().unwrap();
244        let config_path = temp.path().join("invalid.toml");
245
246        #[derive(serde::Deserialize)]
247        struct TestConfig {
248            #[allow(dead_code)]
249            name: String,
250        }
251
252        let invalid_toml = "invalid = toml {";
253        std::fs::write(&config_path, invalid_toml).unwrap();
254
255        let result: Result<TestConfig> = parse_config(&config_path);
256        assert!(result.is_err());
257    }
258}