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//! # Error Handling
45//!
46//! The parser provides detailed error messages that include:
47//!
48//! - **File Context**: Which file failed to parse
49//! - **Operation Context**: Whether it was a read or parse failure
50//! - **Underlying Error**: The specific I/O or TOML parsing error
51//!
52//! Example error output:
53//! ```text
54//! Failed to parse config file: /path/to/config.toml
55//! Caused by:
56//!     invalid TOML value, expected string
57//! ```
58//!
59//! # Integration
60//!
61//! This parser is used throughout AGPM for:
62//!
63//! - Generic configuration file parsing
64//! - Test fixtures and development tools
65
66use anyhow::{Context, Result};
67use std::path::Path;
68
69/// Parse a TOML configuration file into the specified type.
70///
71/// Generic function that reads a TOML file and deserializes it into any type
72/// that implements [`serde::de::DeserializeOwned`]. Provides enhanced error
73/// messages that include the file path context.
74///
75/// # Type Parameters
76///
77/// - `T`: The target type that implements `DeserializeOwned`
78///
79/// # Parameters
80///
81/// - `path`: Path to the TOML configuration file to parse
82///
83/// # Returns
84///
85/// The parsed configuration object of type `T`.
86///
87/// # Examples
88///
89/// ## Basic Usage
90///
91/// ```rust,no_run
92/// use agpm_cli::config::parse_config;
93/// use serde::Deserialize;
94/// use std::path::Path;
95///
96/// #[derive(Deserialize)]
97/// struct Config {
98///     name: String,
99///     port: u16,
100/// }
101///
102/// # fn example() -> anyhow::Result<()> {
103/// let config: Config = parse_config(Path::new("server.toml"))?;
104/// println!("Starting {} on port {}", config.name, config.port);
105/// # Ok(())
106/// # }
107/// ```
108///
109/// ## Error Handling
110///
111/// ```rust,no_run
112/// use agpm_cli::config::parse_config;
113/// use serde::Deserialize;
114/// use std::path::Path;
115///
116/// #[derive(Deserialize)]
117/// struct Config { name: String }
118///
119/// # fn example() {
120/// match parse_config::<Config>(Path::new("missing.toml")) {
121///     Ok(config) => println!("Config loaded: {}", config.name),
122///     Err(e) => eprintln!("Failed to load config: {}", e),
123/// }
124/// # }
125/// ```
126///
127/// # Error Conditions
128///
129/// This function returns an error if:
130///
131/// ## File System Errors
132/// - File does not exist
133/// - Insufficient permissions to read the file
134/// - I/O errors during file reading
135/// - Path is a directory, not a file
136///
137/// ## Parsing Errors
138/// - File contains invalid TOML syntax
139/// - TOML structure doesn't match the target type `T`
140/// - Required fields are missing
141/// - Field types don't match expectations
142/// - TOML contains unsupported features for the target type
143///
144/// # Error Messages
145///
146/// The function provides two levels of error context:
147///
148/// 1. **File Operation Context**: "Failed to read config file: /path/to/file.toml"
149/// 2. **Parsing Context**: "Failed to parse config file: /path/to/file.toml"
150///
151/// The underlying error (file system or TOML parsing) is preserved as the cause.
152///
153/// # Performance
154///
155/// - Reads the entire file into memory before parsing
156/// - TOML parsing is generally fast for typical configuration file sizes
157/// - No caching - each call performs a fresh read and parse
158///
159/// # Thread Safety
160///
161/// This function is thread-safe and can be called concurrently from multiple threads.
162/// Each call operates independently on the file system.
163pub fn parse_config<T>(path: &Path) -> Result<T>
164where
165    T: serde::de::DeserializeOwned,
166{
167    let content = std::fs::read_to_string(path)
168        .with_context(|| format!("Failed to read config file: {}", path.display()))?;
169
170    let config: T = toml::from_str(&content)
171        .with_context(|| format!("Failed to parse config file: {}", path.display()))?;
172
173    Ok(config)
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_parse_config() {
182        use tempfile::tempdir;
183
184        let temp = tempdir().unwrap();
185        let config_path = temp.path().join("test.toml");
186
187        #[derive(serde::Deserialize)]
188        struct TestConfig {
189            name: String,
190            value: i32,
191        }
192
193        let toml_content = r#"
194            name = "test"
195            value = 42
196        "#;
197
198        std::fs::write(&config_path, toml_content).unwrap();
199
200        let config: TestConfig = parse_config(&config_path).unwrap();
201        assert_eq!(config.name, "test");
202        assert_eq!(config.value, 42);
203    }
204
205    #[test]
206    fn test_parse_config_error() {
207        use tempfile::tempdir;
208
209        let temp = tempdir().unwrap();
210        let config_path = temp.path().join("invalid.toml");
211
212        #[derive(serde::Deserialize)]
213        struct TestConfig {
214            #[allow(dead_code)]
215            // Field used by serde for deserialization validation, not accessed directly
216            name: String,
217        }
218
219        let invalid_toml = "invalid = toml {";
220        std::fs::write(&config_path, invalid_toml).unwrap();
221
222        let result: Result<TestConfig> = parse_config(&config_path);
223        assert!(result.is_err());
224    }
225}