syncable_cli/analyzer/helmlint/parser/
chart.rs1use std::collections::HashMap;
6use std::path::Path;
7
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Eq, Default)]
12pub enum ApiVersion {
13 V1,
15 #[default]
17 V2,
18 Unknown(String),
20}
21
22impl<'de> Deserialize<'de> for ApiVersion {
23 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
24 where
25 D: serde::Deserializer<'de>,
26 {
27 let s = String::deserialize(deserializer)?;
28 Ok(match s.as_str() {
29 "v1" => ApiVersion::V1,
30 "v2" => ApiVersion::V2,
31 other => ApiVersion::Unknown(other.to_string()),
32 })
33 }
34}
35
36impl Serialize for ApiVersion {
37 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
38 where
39 S: serde::Serializer,
40 {
41 match self {
42 ApiVersion::V1 => serializer.serialize_str("v1"),
43 ApiVersion::V2 => serializer.serialize_str("v2"),
44 ApiVersion::Unknown(s) => serializer.serialize_str(s),
45 }
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
51#[serde(rename_all = "lowercase")]
52pub enum ChartType {
53 #[default]
55 Application,
56 Library,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
62pub struct Maintainer {
63 pub name: String,
65 pub email: Option<String>,
67 pub url: Option<String>,
69}
70
71#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
73pub struct Dependency {
74 pub name: String,
76 pub version: Option<String>,
78 pub repository: Option<String>,
80 pub condition: Option<String>,
82 pub tags: Option<Vec<String>>,
84 #[serde(rename = "import-values")]
86 pub import_values: Option<Vec<serde_yaml::Value>>,
87 pub alias: Option<String>,
89}
90
91#[derive(Debug, Clone, Deserialize, Serialize)]
93pub struct ChartMetadata {
94 #[serde(rename = "apiVersion")]
96 pub api_version: ApiVersion,
97
98 pub name: String,
100
101 pub version: String,
103
104 #[serde(rename = "kubeVersion")]
106 pub kube_version: Option<String>,
107
108 pub description: Option<String>,
110
111 #[serde(rename = "type")]
113 pub chart_type: Option<ChartType>,
114
115 #[serde(default)]
117 pub keywords: Vec<String>,
118
119 pub home: Option<String>,
121
122 #[serde(default)]
124 pub sources: Vec<String>,
125
126 #[serde(default)]
128 pub dependencies: Vec<Dependency>,
129
130 #[serde(default)]
132 pub maintainers: Vec<Maintainer>,
133
134 pub icon: Option<String>,
136
137 #[serde(rename = "appVersion")]
139 pub app_version: Option<String>,
140
141 pub deprecated: Option<bool>,
143
144 #[serde(default)]
146 pub annotations: HashMap<String, String>,
147}
148
149impl ChartMetadata {
150 pub fn has_valid_api_version(&self) -> bool {
152 matches!(self.api_version, ApiVersion::V1 | ApiVersion::V2)
153 }
154
155 pub fn is_v2(&self) -> bool {
157 matches!(self.api_version, ApiVersion::V2)
158 }
159
160 pub fn is_library(&self) -> bool {
162 matches!(self.chart_type, Some(ChartType::Library))
163 }
164
165 pub fn is_deprecated(&self) -> bool {
167 self.deprecated.unwrap_or(false)
168 }
169
170 pub fn dependency_names(&self) -> Vec<&str> {
172 self.dependencies.iter().map(|d| d.name.as_str()).collect()
173 }
174
175 pub fn has_duplicate_dependencies(&self) -> Vec<&str> {
177 let mut seen = std::collections::HashSet::new();
178 let mut duplicates = Vec::new();
179 for dep in &self.dependencies {
180 let name = dep.alias.as_ref().unwrap_or(&dep.name);
181 if !seen.insert(name.as_str()) {
182 duplicates.push(name.as_str());
183 }
184 }
185 duplicates
186 }
187}
188
189#[derive(Debug)]
191pub struct ChartParseError {
192 pub message: String,
193 pub line: Option<u32>,
194}
195
196impl std::fmt::Display for ChartParseError {
197 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198 if let Some(line) = self.line {
199 write!(f, "line {}: {}", line, self.message)
200 } else {
201 write!(f, "{}", self.message)
202 }
203 }
204}
205
206impl std::error::Error for ChartParseError {}
207
208pub fn parse_chart_yaml(content: &str) -> Result<ChartMetadata, ChartParseError> {
210 serde_yaml::from_str(content).map_err(|e| {
211 let line = e.location().map(|l| l.line() as u32);
212 ChartParseError {
213 message: e.to_string(),
214 line,
215 }
216 })
217}
218
219pub fn parse_chart_yaml_file(path: &Path) -> Result<ChartMetadata, ChartParseError> {
221 let content = std::fs::read_to_string(path).map_err(|e| ChartParseError {
222 message: format!("Failed to read file: {}", e),
223 line: None,
224 })?;
225 parse_chart_yaml(&content)
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn test_parse_minimal_chart() {
234 let yaml = r#"
235apiVersion: v2
236name: test-chart
237version: 0.1.0
238"#;
239 let chart = parse_chart_yaml(yaml).unwrap();
240 assert_eq!(chart.name, "test-chart");
241 assert_eq!(chart.version, "0.1.0");
242 assert!(chart.is_v2());
243 }
244
245 #[test]
246 fn test_parse_full_chart() {
247 let yaml = r#"
248apiVersion: v2
249name: my-app
250version: 1.2.3
251kubeVersion: ">=1.19.0"
252description: A sample application
253type: application
254keywords:
255 - app
256 - example
257home: https://example.com
258sources:
259 - https://github.com/example/my-app
260maintainers:
261 - name: John Doe
262 email: john@example.com
263icon: https://example.com/icon.png
264appVersion: "2.0.0"
265dependencies:
266 - name: postgresql
267 version: "~11.0"
268 repository: https://charts.bitnami.com/bitnami
269annotations:
270 category: backend
271"#;
272 let chart = parse_chart_yaml(yaml).unwrap();
273 assert_eq!(chart.name, "my-app");
274 assert_eq!(chart.version, "1.2.3");
275 assert_eq!(chart.kube_version, Some(">=1.19.0".to_string()));
276 assert_eq!(chart.description, Some("A sample application".to_string()));
277 assert!(!chart.is_library());
278 assert_eq!(chart.keywords.len(), 2);
279 assert_eq!(chart.maintainers.len(), 1);
280 assert_eq!(chart.dependencies.len(), 1);
281 }
282
283 #[test]
284 fn test_parse_library_chart() {
285 let yaml = r#"
286apiVersion: v2
287name: common
288version: 1.0.0
289type: library
290"#;
291 let chart = parse_chart_yaml(yaml).unwrap();
292 assert!(chart.is_library());
293 }
294
295 #[test]
296 fn test_parse_v1_chart() {
297 let yaml = r#"
298apiVersion: v1
299name: legacy-chart
300version: 1.0.0
301"#;
302 let chart = parse_chart_yaml(yaml).unwrap();
303 assert!(!chart.is_v2());
304 assert!(chart.has_valid_api_version());
305 }
306
307 #[test]
308 fn test_deprecated_chart() {
309 let yaml = r#"
310apiVersion: v2
311name: old-chart
312version: 1.0.0
313deprecated: true
314"#;
315 let chart = parse_chart_yaml(yaml).unwrap();
316 assert!(chart.is_deprecated());
317 }
318
319 #[test]
320 fn test_duplicate_dependencies() {
321 let yaml = r#"
322apiVersion: v2
323name: test
324version: 1.0.0
325dependencies:
326 - name: redis
327 version: "1.0.0"
328 repository: https://charts.bitnami.com/bitnami
329 - name: redis
330 version: "2.0.0"
331 repository: https://charts.bitnami.com/bitnami
332"#;
333 let chart = parse_chart_yaml(yaml).unwrap();
334 let duplicates = chart.has_duplicate_dependencies();
335 assert_eq!(duplicates.len(), 1);
336 assert_eq!(duplicates[0], "redis");
337 }
338
339 #[test]
340 fn test_parse_error() {
341 let yaml = "invalid: [yaml";
342 let result = parse_chart_yaml(yaml);
343 assert!(result.is_err());
344 }
345}