1#![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
12pub type Result<T> = std::result::Result<T, ConfigError>;
14
15#[derive(Debug, thiserror::Error)]
17pub enum ConfigError {
18 #[error("Configuration file not found: {0}")]
20 FileNotFound(PathBuf),
21
22 #[error("I/O error reading configuration: {0}")]
24 Io(#[from] std::io::Error),
25
26 #[error("TOML parsing error: {0}")]
28 TomlParse(#[from] toml::de::Error),
29
30 #[error("TOML serialization error: {0}")]
32 TomlSerialize(#[from] toml::ser::Error),
33
34 #[error("Configuration validation error: {0}")]
36 Validation(String),
37
38 #[error("Missing required field: {0}")]
40 MissingField(String),
41
42 #[error("Invalid value for field '{field}': {reason}")]
44 InvalidValue {
45 field: String,
47 reason: String,
49 },
50
51 #[error("Environment variable error: {0}")]
53 EnvVar(String),
54
55 #[error("Workspace configuration error: {0}")]
57 Workspace(String),
58
59 #[error("{0}")]
61 Other(String),
62}
63
64#[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#[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}