Skip to main content

ggen_config/config_lib/
error.rs

1//! Error types for configuration parsing and validation
2
3#![allow(
4    clippy::uninlined_format_args,
5    clippy::doc_markdown,
6    clippy::too_many_lines,
7    clippy::match_same_arms
8)]
9
10use std::path::PathBuf;
11
12/// Result type alias for configuration operations
13pub type Result<T> = std::result::Result<T, ConfigError>;
14
15/// Errors that can occur during configuration operations
16#[derive(Debug, thiserror::Error)]
17pub enum ConfigError {
18    /// Configuration file not found
19    #[error("Configuration file not found: {0}")]
20    FileNotFound(PathBuf),
21
22    /// I/O error reading configuration
23    #[error("I/O error reading configuration: {0}")]
24    Io(#[from] std::io::Error),
25
26    /// TOML parsing error
27    #[error("TOML parsing error: {0}")]
28    TomlParse(#[from] toml::de::Error),
29
30    /// TOML serialization error
31    #[error("TOML serialization error: {0}")]
32    TomlSerialize(#[from] toml::ser::Error),
33
34    /// Configuration validation error
35    #[error("Configuration validation error: {0}")]
36    Validation(String),
37
38    /// Missing required field
39    #[error("Missing required field: {0}")]
40    MissingField(String),
41
42    /// Invalid value for field
43    #[error("Invalid value for field '{field}': {reason}")]
44    InvalidValue {
45        /// Field name
46        field: String,
47        /// Reason for invalidity
48        reason: String,
49    },
50
51    /// Environment variable expansion error
52    #[error("Environment variable error: {0}")]
53    EnvVar(String),
54
55    /// Workspace configuration error
56    #[error("Workspace configuration error: {0}")]
57    Workspace(String),
58
59    /// Generic error
60    #[error("{0}")]
61    Other(String),
62}
63
64/// Format a single star_toml ValidationError to the legacy string format.
65#[must_use]
66pub fn format_single_star_toml_error(err: &star_toml::ValidationError) -> String {
67    let loc_str = err.loc.to_string();
68    let code = err.code();
69    let input_val = err.input.as_deref().unwrap_or("");
70
71    match (loc_str.as_str(), code) {
72        ("project.name", "empty") => "Project name cannot be empty".to_string(),
73        ("project.version", "invalid_semver") => format!(
74            "Invalid version format: '{}'. Expected semver format (e.g., 1.0.0)",
75            input_val
76        ),
77        ("ai.provider", "not_one_of") => format!(
78            "Unknown AI provider: '{}'. Valid providers: [\"openai\", \"ollama\", \"anthropic\", \"cohere\", \"huggingface\"]",
79            input_val
80        ),
81        ("ai.temperature", "out_of_range") => format!(
82            "AI temperature must be between 0.0 and 1.0, got {}",
83            input_val
84        ),
85        ("ai.max_tokens", "out_of_range") => "AI max_tokens must be greater than 0".to_string(),
86        ("ai.timeout", "out_of_range") => "AI timeout must be greater than 0".to_string(),
87        ("ai.validation.quality_threshold", "out_of_range") => format!(
88            "AI validation quality_threshold must be between 0.0 and 1.0, got {}",
89            input_val
90        ),
91        ("templates.directory", "invalid_path") => {
92            if input_val.contains('\0') {
93                format!(
94                    "Invalid path '{}': path must not contain null bytes",
95                    input_val
96                )
97            } else if input_val.contains("..") {
98                format!(
99                    "Invalid path '{}': path traversal ('..') is not allowed",
100                    input_val
101                )
102            } else {
103                format!("Invalid path '{}': path must not be empty", input_val)
104            }
105        }
106        ("templates.output_directory", "invalid_path") => {
107            if input_val.contains('\0') {
108                format!(
109                    "Invalid path '{}': path must not contain null bytes",
110                    input_val
111                )
112            } else if input_val.contains("..") {
113                format!(
114                    "Invalid path '{}': path traversal ('..') is not allowed",
115                    input_val
116                )
117            } else {
118                format!("Invalid path '{}': path must not be empty", input_val)
119            }
120        }
121        ("performance.max_workers", "out_of_range") => {
122            "Performance max_workers must be greater than 0 when parallel_execution is enabled"
123                .to_string()
124        }
125        ("performance.cache_size", "invalid_size_format") => format!(
126            "Invalid cache_size format: '{}'. Expected format like '1GB', '512MB'",
127            input_val
128        ),
129        ("logging.level", "not_one_of") => format!(
130            "Invalid log level: '{}'. Valid levels: [\"trace\", \"debug\", \"info\", \"warn\", \"error\"]",
131            input_val
132        ),
133        ("logging.format", "not_one_of") => format!(
134            "Invalid log format: '{}'. Valid formats: [\"json\", \"text\", \"pretty\"]",
135            input_val
136        ),
137        ("logging.file", "invalid_path") => {
138            if input_val.contains('\0') {
139                format!(
140                    "Invalid path '{}': path must not contain null bytes",
141                    input_val
142                )
143            } else if input_val.contains("..") {
144                format!(
145                    "Invalid path '{}': path traversal ('..') is not allowed",
146                    input_val
147                )
148            } else {
149                format!("Invalid path '{}': path must not be empty", input_val)
150            }
151        }
152        ("mcp.transport.transport_type", "not_one_of") => format!(
153            "Invalid MCP transport type: '{}'. Valid types: [\"stdio\", \"http\", \"websocket\"]",
154            input_val
155        ),
156        ("mcp.transport.port", "out_of_range") => format!(
157            "Invalid MCP port: {}. Must be between 1 and 65535",
158            input_val
159        ),
160        ("mcp.transport.tls.cert_path", "invalid_path") => {
161            if input_val.contains('\0') {
162                format!(
163                    "Invalid path '{}': path must not contain null bytes",
164                    input_val
165                )
166            } else if input_val.contains("..") {
167                format!(
168                    "Invalid path '{}': path traversal ('..') is not allowed",
169                    input_val
170                )
171            } else {
172                format!("Invalid path '{}': path must not be empty", input_val)
173            }
174        }
175        ("mcp.transport.tls.key_path", "invalid_path") => {
176            if input_val.contains('\0') {
177                format!(
178                    "Invalid path '{}': path must not contain null bytes",
179                    input_val
180                )
181            } else if input_val.contains("..") {
182                format!(
183                    "Invalid path '{}': path traversal ('..') is not allowed",
184                    input_val
185                )
186            } else {
187                format!("Invalid path '{}': path must not be empty", input_val)
188            }
189        }
190        ("mcp.transport.tls.ca_path", "invalid_path") => {
191            if input_val.contains('\0') {
192                format!(
193                    "Invalid path '{}': path must not contain null bytes",
194                    input_val
195                )
196            } else if input_val.contains("..") {
197                format!(
198                    "Invalid path '{}': path traversal ('..') is not allowed",
199                    input_val
200                )
201            } else {
202                format!("Invalid path '{}': path must not be empty", input_val)
203            }
204        }
205        ("mcp.tools.discovery_path", "invalid_path") => {
206            if input_val.contains('\0') {
207                format!(
208                    "Invalid path '{}': path must not contain null bytes",
209                    input_val
210                )
211            } else if input_val.contains("..") {
212                format!(
213                    "Invalid path '{}': path traversal ('..') is not allowed",
214                    input_val
215                )
216            } else {
217                format!("Invalid path '{}': path must not be empty", input_val)
218            }
219        }
220        ("mcp.tool_timeout_ms", "out_of_range") => {
221            "MCP tool_timeout_ms must be greater than 0".to_string()
222        }
223        ("mcp.max_concurrent_requests", "out_of_range") => {
224            "MCP max_concurrent_requests must be greater than 0".to_string()
225        }
226        ("a2a.transport.transport_type", "not_one_of") => format!(
227            "Invalid A2A transport type: '{}'. Valid types: [\"memory\", \"http\", \"websocket\", \"amqp\"]",
228            input_val
229        ),
230        ("a2a.transport.port", "out_of_range") => format!(
231            "Invalid A2A port: {}. Must be between 1 and 65535",
232            input_val
233        ),
234        ("a2a.messaging.persistence_path", "invalid_path") => {
235            if input_val.contains('\0') {
236                format!(
237                    "Invalid path '{}': path must not contain null bytes",
238                    input_val
239                )
240            } else if input_val.contains("..") {
241                format!(
242                    "Invalid path '{}': path traversal ('..') is not allowed",
243                    input_val
244                )
245            } else {
246                format!("Invalid path '{}': path must not be empty", input_val)
247            }
248        }
249        ("a2a.orchestration.mode", "not_one_of") => format!(
250            "Invalid A2A orchestration mode: '{}'. Valid modes: [\"centralized\", \"decentralized\", \"hierarchical\"]",
251            input_val
252        ),
253        ("a2a.orchestration.consensus_algorithm", "not_one_of") => format!(
254            "Invalid A2A consensus algorithm: '{}'. Valid: [\"raft\", \"pbft\", \"naive\"]",
255            input_val
256        ),
257        ("a2a.orchestration.consensus_algorithm", "missing") => {
258            "A2A consensus algorithm must be specified when consensus is enabled".to_string()
259        }
260        _ => {
261            if let Some(input) = &err.input {
262                format!("{}: {} (got: `{}`)", loc_str, err.msg, input)
263            } else {
264                format!("{}: {}", loc_str, err.msg)
265            }
266        }
267    }
268}
269
270/// Format star_toml ValidationErrors to the legacy string format.
271#[must_use]
272pub fn format_star_toml_errors(errs: &star_toml::ValidationErrors) -> String {
273    let mut formatted = Vec::new();
274    for err in errs.errors() {
275        formatted.push(format_single_star_toml_error(err));
276    }
277    formatted.join("; ")
278}
279
280impl From<star_toml::Error> for ConfigError {
281    fn from(err: star_toml::Error) -> Self {
282        match err {
283            star_toml::Error::FileNotFound(path) => ConfigError::FileNotFound(path),
284            star_toml::Error::Io { source, .. } => ConfigError::Io(source),
285            star_toml::Error::Parse { source, .. } => ConfigError::TomlParse(source),
286            star_toml::Error::Serialize(source) => ConfigError::TomlSerialize(source),
287            star_toml::Error::Validation { reason, .. } => ConfigError::Validation(reason),
288            star_toml::Error::Invalid(errs) => {
289                ConfigError::Validation(format_star_toml_errors(&errs))
290            }
291        }
292    }
293}