sherpack_convert/
error.rs

1//! Error and warning types for the converter
2//!
3//! This module provides rich error reporting and conversion warnings
4//! with alternatives and documentation links.
5
6use std::path::PathBuf;
7use thiserror::Error;
8
9/// Converter error
10#[derive(Debug, Error)]
11pub enum ConvertError {
12    #[error("IO error: {0}")]
13    Io(#[from] std::io::Error),
14
15    #[error("Failed to parse Chart.yaml: {0}")]
16    ChartParse(#[from] crate::chart::ChartError),
17
18    #[error("Failed to parse template: {0}")]
19    TemplateParse(#[from] crate::parser::ParseError),
20
21    #[error("YAML error: {0}")]
22    Yaml(#[from] serde_yaml::Error),
23
24    #[error("Invalid chart: {0}")]
25    InvalidChart(String),
26
27    #[error("File not found: {0}")]
28    FileNotFound(PathBuf),
29
30    #[error("Directory not found: {0}")]
31    DirectoryNotFound(PathBuf),
32
33    #[error("Not a Helm chart: missing {0}")]
34    NotAChart(String),
35
36    #[error("Output directory already exists: {0}")]
37    OutputExists(PathBuf),
38
39    #[error("Conversion failed for {file}: {message}")]
40    ConversionFailed { file: PathBuf, message: String },
41}
42
43// =============================================================================
44// WARNING SYSTEM
45// =============================================================================
46
47/// Warning severity levels
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
49pub enum WarningSeverity {
50    /// Informational - conversion succeeded, minor syntax change
51    Info,
52    /// Warning - conversion succeeded but manual review recommended
53    Warning,
54    /// Unsupported - feature not available, alternative provided
55    Unsupported,
56    /// Error - conversion failed for this element
57    Error,
58}
59
60impl WarningSeverity {
61    /// Get the display color for terminal output
62    pub fn color(&self) -> &'static str {
63        match self {
64            Self::Info => "cyan",
65            Self::Warning => "yellow",
66            Self::Unsupported => "magenta",
67            Self::Error => "red",
68        }
69    }
70
71    /// Get the emoji icon for this severity
72    pub fn icon(&self) -> &'static str {
73        match self {
74            Self::Info => "ℹ",
75            Self::Warning => "⚠",
76            Self::Unsupported => "✗",
77            Self::Error => "✗",
78        }
79    }
80
81    /// Get the label for this severity
82    pub fn label(&self) -> &'static str {
83        match self {
84            Self::Info => "info",
85            Self::Warning => "warning",
86            Self::Unsupported => "unsupported",
87            Self::Error => "error",
88        }
89    }
90}
91
92/// Warning category for grouping related warnings
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94pub enum WarningCategory {
95    /// Syntax conversion (Go template → Jinja2)
96    Syntax,
97    /// Unsupported function or feature
98    UnsupportedFeature,
99    /// Deprecated pattern (works but not recommended)
100    Deprecated,
101    /// Security concern (crypto in templates, etc.)
102    Security,
103    /// GitOps compatibility issue
104    GitOps,
105    /// Performance consideration
106    Performance,
107}
108
109impl WarningCategory {
110    /// Get the display label
111    pub fn label(&self) -> &'static str {
112        match self {
113            Self::Syntax => "syntax",
114            Self::UnsupportedFeature => "unsupported",
115            Self::Deprecated => "deprecated",
116            Self::Security => "security",
117            Self::GitOps => "gitops",
118            Self::Performance => "performance",
119        }
120    }
121}
122
123/// Rich warning with context and alternatives
124#[derive(Debug, Clone)]
125pub struct ConversionWarning {
126    /// Warning severity
127    pub severity: WarningSeverity,
128    /// Warning category
129    pub category: WarningCategory,
130    /// File where the warning occurred
131    pub file: PathBuf,
132    /// Line number (if applicable)
133    pub line: Option<usize>,
134    /// The Helm pattern that triggered the warning
135    pub pattern: String,
136    /// Human-readable message
137    pub message: String,
138    /// Suggested alternative or fix
139    pub suggestion: Option<String>,
140    /// Link to documentation
141    pub doc_link: Option<String>,
142}
143
144impl ConversionWarning {
145    /// Create an info-level warning
146    pub fn info(file: PathBuf, pattern: &str, message: &str) -> Self {
147        Self {
148            severity: WarningSeverity::Info,
149            category: WarningCategory::Syntax,
150            file,
151            line: None,
152            pattern: pattern.to_string(),
153            message: message.to_string(),
154            suggestion: None,
155            doc_link: None,
156        }
157    }
158
159    /// Create a warning-level warning
160    pub fn warning(file: PathBuf, pattern: &str, message: &str) -> Self {
161        Self {
162            severity: WarningSeverity::Warning,
163            category: WarningCategory::Syntax,
164            file,
165            line: None,
166            pattern: pattern.to_string(),
167            message: message.to_string(),
168            suggestion: None,
169            doc_link: None,
170        }
171    }
172
173    /// Create an unsupported feature warning
174    pub fn unsupported(file: PathBuf, pattern: &str, alternative: &str) -> Self {
175        Self {
176            severity: WarningSeverity::Unsupported,
177            category: WarningCategory::UnsupportedFeature,
178            file,
179            line: None,
180            pattern: pattern.to_string(),
181            message: format!("'{}' is not supported in Sherpack", pattern),
182            suggestion: Some(alternative.to_string()),
183            doc_link: Some("https://sherpack.dev/docs/helm-migration".to_string()),
184        }
185    }
186
187    /// Create a security warning
188    pub fn security(file: PathBuf, pattern: &str, message: &str, alternative: &str) -> Self {
189        Self {
190            severity: WarningSeverity::Unsupported,
191            category: WarningCategory::Security,
192            file,
193            line: None,
194            pattern: pattern.to_string(),
195            message: message.to_string(),
196            suggestion: Some(alternative.to_string()),
197            doc_link: Some("https://sherpack.dev/docs/security-best-practices".to_string()),
198        }
199    }
200
201    /// Create a GitOps compatibility warning
202    pub fn gitops(file: PathBuf, pattern: &str, message: &str, alternative: &str) -> Self {
203        Self {
204            severity: WarningSeverity::Warning,
205            category: WarningCategory::GitOps,
206            file,
207            line: None,
208            pattern: pattern.to_string(),
209            message: message.to_string(),
210            suggestion: Some(alternative.to_string()),
211            doc_link: Some("https://sherpack.dev/docs/gitops-compatibility".to_string()),
212        }
213    }
214
215    /// Add line number to warning
216    pub fn at_line(mut self, line: usize) -> Self {
217        self.line = Some(line);
218        self
219    }
220
221    /// Add suggestion to warning
222    pub fn with_suggestion(mut self, suggestion: &str) -> Self {
223        self.suggestion = Some(suggestion.to_string());
224        self
225    }
226
227    /// Add documentation link
228    pub fn with_doc_link(mut self, url: &str) -> Self {
229        self.doc_link = Some(url.to_string());
230        self
231    }
232
233    /// Set category
234    pub fn with_category(mut self, category: WarningCategory) -> Self {
235        self.category = category;
236        self
237    }
238}
239
240impl std::fmt::Display for ConversionWarning {
241    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242        // Format: [severity] file:line - message
243        write!(f, "[{}] ", self.severity.label())?;
244        write!(f, "{}", self.file.display())?;
245
246        if let Some(line) = self.line {
247            write!(f, ":{}", line)?;
248        }
249
250        write!(f, " - {}", self.message)?;
251
252        if let Some(ref suggestion) = self.suggestion {
253            write!(f, "\n  {} {}", self.severity.icon(), suggestion)?;
254        }
255
256        if let Some(ref link) = self.doc_link {
257            write!(f, "\n  See: {}", link)?;
258        }
259
260        Ok(())
261    }
262}
263
264// =============================================================================
265// CONVERSION RESULT
266// =============================================================================
267
268/// Summary of conversion results
269#[derive(Debug, Default)]
270pub struct ConversionSummary {
271    /// Number of files successfully converted
272    pub files_converted: usize,
273    /// Number of files copied (non-template files)
274    pub files_copied: usize,
275    /// Number of files skipped
276    pub files_skipped: usize,
277    /// All warnings generated during conversion
278    pub warnings: Vec<ConversionWarning>,
279}
280
281impl ConversionSummary {
282    /// Create a new empty summary
283    pub fn new() -> Self {
284        Self::default()
285    }
286
287    /// Add a warning
288    pub fn add_warning(&mut self, warning: ConversionWarning) {
289        self.warnings.push(warning);
290    }
291
292    /// Get warnings grouped by severity
293    pub fn warnings_by_severity(
294        &self,
295    ) -> std::collections::HashMap<WarningSeverity, Vec<&ConversionWarning>> {
296        let mut grouped = std::collections::HashMap::new();
297        for warning in &self.warnings {
298            grouped
299                .entry(warning.severity)
300                .or_insert_with(Vec::new)
301                .push(warning);
302        }
303        grouped
304    }
305
306    /// Get warnings grouped by category
307    pub fn warnings_by_category(
308        &self,
309    ) -> std::collections::HashMap<WarningCategory, Vec<&ConversionWarning>> {
310        let mut grouped = std::collections::HashMap::new();
311        for warning in &self.warnings {
312            grouped
313                .entry(warning.category)
314                .or_insert_with(Vec::new)
315                .push(warning);
316        }
317        grouped
318    }
319
320    /// Get count of warnings by severity
321    pub fn count_by_severity(&self, severity: WarningSeverity) -> usize {
322        self.warnings
323            .iter()
324            .filter(|w| w.severity == severity)
325            .count()
326    }
327
328    /// Check if there are any errors
329    pub fn has_errors(&self) -> bool {
330        self.warnings
331            .iter()
332            .any(|w| w.severity == WarningSeverity::Error)
333    }
334
335    /// Check if there are any unsupported features
336    pub fn has_unsupported(&self) -> bool {
337        self.warnings
338            .iter()
339            .any(|w| w.severity == WarningSeverity::Unsupported)
340    }
341
342    /// Get a success message
343    pub fn success_message(&self) -> String {
344        let mut msg = format!(
345            "Converted {} file{}, copied {} file{}",
346            self.files_converted,
347            if self.files_converted == 1 { "" } else { "s" },
348            self.files_copied,
349            if self.files_copied == 1 { "" } else { "s" },
350        );
351
352        if self.files_skipped > 0 {
353            msg.push_str(&format!(", skipped {}", self.files_skipped));
354        }
355
356        let info_count = self.count_by_severity(WarningSeverity::Info);
357        let warning_count = self.count_by_severity(WarningSeverity::Warning);
358        let unsupported_count = self.count_by_severity(WarningSeverity::Unsupported);
359
360        if info_count + warning_count + unsupported_count > 0 {
361            msg.push_str(&format!(
362                " with {} warning{}",
363                info_count + warning_count + unsupported_count,
364                if info_count + warning_count + unsupported_count == 1 {
365                    ""
366                } else {
367                    "s"
368                }
369            ));
370        }
371
372        msg
373    }
374}
375
376/// Result type for conversion operations
377pub type Result<T> = std::result::Result<T, ConvertError>;
378
379// =============================================================================
380// PREDEFINED WARNINGS FOR COMMON PATTERNS
381// =============================================================================
382
383/// Factory functions for common warnings
384pub mod warnings {
385    use super::*;
386    use std::path::Path;
387
388    /// Create warning for crypto functions (genCA, genPrivateKey, etc.)
389    pub fn crypto_in_template(file: &Path, func_name: &str) -> ConversionWarning {
390        ConversionWarning::security(
391            file.to_path_buf(),
392            func_name,
393            &format!(
394                "'{}' generates cryptographic material in templates - this is insecure",
395                func_name
396            ),
397            "Use cert-manager for certificates or external-secrets for keys",
398        )
399    }
400
401    /// Create warning for Files.Get/Glob
402    pub fn files_access(file: &Path, method: &str) -> ConversionWarning {
403        ConversionWarning::unsupported(
404            file.to_path_buf(),
405            &format!(".Files.{}", method),
406            "Embed file content in values.yaml or use ConfigMap/Secret resources",
407        )
408        .with_category(WarningCategory::UnsupportedFeature)
409    }
410
411    /// Create warning for lookup function
412    pub fn lookup_function(file: &Path) -> ConversionWarning {
413        ConversionWarning::gitops(
414            file.to_path_buf(),
415            "lookup",
416            "'lookup' queries the cluster at render time - incompatible with GitOps",
417            "Returns {} in template mode. Use explicit values for GitOps compatibility.",
418        )
419    }
420
421    /// Create warning for tpl with dynamic input
422    pub fn dynamic_tpl(file: &Path) -> ConversionWarning {
423        ConversionWarning::warning(
424            file.to_path_buf(),
425            "tpl",
426            "'tpl' with dynamic input may have security implications",
427        )
428        .with_suggestion("Sherpack limits tpl recursion depth to 10 for safety")
429        .with_doc_link("https://sherpack.dev/docs/template-security")
430    }
431
432    /// Create warning for getHostByName
433    pub fn dns_lookup(file: &Path) -> ConversionWarning {
434        ConversionWarning::gitops(
435            file.to_path_buf(),
436            "getHostByName",
437            "'getHostByName' performs DNS lookup at render time - non-deterministic",
438            "Use explicit IP address or hostname in values.yaml",
439        )
440    }
441
442    /// Create warning for random functions
443    pub fn random_function(file: &Path, func_name: &str) -> ConversionWarning {
444        ConversionWarning::gitops(
445            file.to_path_buf(),
446            func_name,
447            &format!(
448                "'{}' generates different values on each render - breaks GitOps",
449                func_name
450            ),
451            "Pre-generate values and store in values.yaml or use external-secrets",
452        )
453    }
454
455    /// Create info for successful syntax conversion
456    pub fn syntax_converted(file: &Path, from: &str, to: &str) -> ConversionWarning {
457        ConversionWarning::info(
458            file.to_path_buf(),
459            from,
460            &format!("Converted '{}' to '{}'", from, to),
461        )
462        .with_category(WarningCategory::Syntax)
463    }
464
465    /// Create warning for 'with' block context issues
466    pub fn with_block_context(file: &Path) -> ConversionWarning {
467        ConversionWarning::warning(
468            file.to_path_buf(),
469            "with",
470            "'with' block context scoping differs between Go templates and Jinja2",
471        )
472        .with_suggestion("Review converted template - use explicit variable names if needed")
473        .with_category(WarningCategory::Syntax)
474    }
475
476    /// Create info for macro conversion
477    pub fn macro_converted(file: &Path, helm_name: &str, jinja_name: &str) -> ConversionWarning {
478        ConversionWarning::info(
479            file.to_path_buf(),
480            &format!("define \"{}\"", helm_name),
481            &format!("Converted to Jinja2 macro '{}'", jinja_name),
482        )
483        .with_category(WarningCategory::Syntax)
484    }
485}