Skip to main content

eggress_pproxy_compat/
issues.rs

1//! Canonical typed compatibility issue model.
2//!
3//! [`CompatIssue`] is the single stored representation for every translation
4//! warning, unsupported feature, and informational diagnostic produced by the
5//! compatibility layer. It carries the severity disposition, the stable
6//! [`DiagnosticCode`], the original string tag (warning category or feature
7//! id) for lossless round-trips to the legacy view types, the manifest
8//! feature/tier mapping where applicable, the redacted human-readable
9//! message, and an optional remediation suggestion.
10//!
11//! Human text, JSON output ([`StructuredDiagnostic`]), warning collection
12//! ([`CompatWarning`]), and CLI rendering are all views over this model:
13//! [`TranslationOutput`] stores `issues` and derives the rest.
14
15use crate::diagnostics::{DiagnosticCode, StructuredDiagnostic};
16use crate::warnings::{CompatWarning, UnsupportedFeature};
17
18/// Severity/disposition of a compatibility issue.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum IssueSeverity {
21    /// Degraded but runnable behavior; startup proceeds.
22    Warning,
23    /// Blocking behavior; startup is refused by the execution gate.
24    Unsupported,
25    /// Informational note; no behavior impact.
26    Info,
27}
28
29impl std::fmt::Display for IssueSeverity {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        match self {
32            Self::Warning => f.write_str("warning"),
33            Self::Unsupported => f.write_str("unsupported"),
34            Self::Info => f.write_str("info"),
35        }
36    }
37}
38
39/// One typed compatibility issue: the canonical stored diagnostic unit.
40#[derive(Debug, Clone, PartialEq)]
41pub struct CompatIssue {
42    /// Warning (proceed) vs unsupported (block) vs info.
43    pub severity: IssueSeverity,
44    /// Stable diagnostic code used by JSON output, tests, and docs.
45    pub code: DiagnosticCode,
46    /// Original warning category tag (e.g. `"verbose-mode"`), if warning-born.
47    pub category: Option<&'static str>,
48    /// Original unsupported feature tag (e.g. `"daemon"`), if blocking.
49    pub feature: Option<&'static str>,
50    /// Manifest feature id, if this issue maps to a known feature.
51    pub feature_id: Option<String>,
52    /// Compatibility tier (`drop_in`, `compatible_with_warning`,
53    /// `native_equivalent`, `intentional_non_parity`, `unsupported`).
54    pub tier: Option<String>,
55    /// Human-readable description (credentials are redacted at construction).
56    pub message: String,
57    /// Suggested eggress-native alternative, if applicable.
58    pub suggestion: Option<String>,
59}
60
61impl CompatIssue {
62    /// Classify a legacy warning into a typed issue (severity `Warning`).
63    ///
64    /// Classification funnels through the single
65    /// `StructuredDiagnostic::from(&CompatWarning)` table so string-category
66    /// dispatch exists in exactly one place.
67    pub fn warning(warn: CompatWarning) -> Self {
68        let category = warn.category;
69        let diag = StructuredDiagnostic::from(&warn);
70        Self {
71            severity: IssueSeverity::Warning,
72            code: diag.code,
73            category: Some(category),
74            feature: None,
75            feature_id: diag.feature_id,
76            tier: diag.tier,
77            message: diag.message,
78            suggestion: diag.suggestion,
79        }
80    }
81
82    /// Classify a legacy unsupported feature into a typed issue (severity
83    /// `Unsupported`).
84    pub fn unsupported(item: UnsupportedFeature) -> Self {
85        let feature = item.feature;
86        let code = crate::diagnostics::classify_unsupported_feature_code(feature);
87        let tier = crate::diagnostics::classify_unsupported_feature_tier(feature);
88        // Reuse the canonical error-to-diagnostic mapping for message and
89        // suggestion so all renderings agree.
90        let diag = StructuredDiagnostic::from(crate::error::CompatError::unsupported(
91            feature,
92            item.detail.clone(),
93        ));
94        debug_assert_eq!(diag.code, code);
95        Self {
96            severity: IssueSeverity::Unsupported,
97            code,
98            category: None,
99            feature: Some(feature),
100            feature_id: Some(feature.to_string()),
101            tier: Some(tier.to_string()),
102            message: diag.message,
103            suggestion: diag.suggestion,
104        }
105    }
106
107    /// Convenience constructor for warning-category call sites.
108    pub fn warning_category(category: &'static str, message: impl Into<String>) -> Self {
109        Self::warning(CompatWarning {
110            category,
111            message: message.into(),
112        })
113    }
114
115    /// Convenience constructor for unsupported-feature call sites.
116    pub fn unsupported_feature(feature: &'static str, detail: impl Into<String>) -> Self {
117        Self::unsupported(UnsupportedFeature {
118            feature,
119            detail: detail.into(),
120        })
121    }
122
123    /// View as a legacy [`CompatWarning`] (lossless: category round-trips).
124    pub fn to_warning(&self) -> Option<CompatWarning> {
125        match self.severity {
126            IssueSeverity::Warning => Some(CompatWarning {
127                category: self.category.unwrap_or("general"),
128                message: self.message.clone(),
129            }),
130            IssueSeverity::Unsupported | IssueSeverity::Info => None,
131        }
132    }
133
134    /// View as a legacy [`UnsupportedFeature`] (lossless: feature round-trips).
135    pub fn to_unsupported(&self) -> Option<UnsupportedFeature> {
136        match self.severity {
137            IssueSeverity::Unsupported => Some(UnsupportedFeature {
138                feature: self.feature.unwrap_or("unknown"),
139                detail: self.message.clone(),
140            }),
141            IssueSeverity::Warning | IssueSeverity::Info => None,
142        }
143    }
144
145    /// View as a [`StructuredDiagnostic`] for JSON/human rendering.
146    pub fn to_diagnostic(&self) -> StructuredDiagnostic {
147        StructuredDiagnostic {
148            code: self.code,
149            feature_id: self.feature_id.clone(),
150            tier: self.tier.clone(),
151            message: self.message.clone(),
152            suggestion: self.suggestion.clone(),
153        }
154    }
155}
156
157impl std::fmt::Display for CompatIssue {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        write!(f, "[{}:{}] {}", self.severity, self.code, self.message)?;
160        if let Some(ref tier) = self.tier {
161            write!(f, " (tier: {})", tier)?;
162        }
163        if let Some(ref suggestion) = self.suggestion {
164            write!(f, " — suggestion: {}", suggestion)?;
165        }
166        Ok(())
167    }
168}