use std::collections::HashMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum ApiVersion {
V1,
#[default]
V2,
Unknown(String),
}
impl<'de> Deserialize<'de> for ApiVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(match s.as_str() {
"v1" => ApiVersion::V1,
"v2" => ApiVersion::V2,
other => ApiVersion::Unknown(other.to_string()),
})
}
}
impl Serialize for ApiVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
ApiVersion::V1 => serializer.serialize_str("v1"),
ApiVersion::V2 => serializer.serialize_str("v2"),
ApiVersion::Unknown(s) => serializer.serialize_str(s),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ChartType {
#[default]
Application,
Library,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct Maintainer {
pub name: String,
pub email: Option<String>,
pub url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct Dependency {
pub name: String,
pub version: Option<String>,
pub repository: Option<String>,
pub condition: Option<String>,
pub tags: Option<Vec<String>>,
#[serde(rename = "import-values")]
pub import_values: Option<Vec<serde_yaml::Value>>,
pub alias: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ChartMetadata {
#[serde(rename = "apiVersion")]
pub api_version: ApiVersion,
pub name: String,
pub version: String,
#[serde(rename = "kubeVersion")]
pub kube_version: Option<String>,
pub description: Option<String>,
#[serde(rename = "type")]
pub chart_type: Option<ChartType>,
#[serde(default)]
pub keywords: Vec<String>,
pub home: Option<String>,
#[serde(default)]
pub sources: Vec<String>,
#[serde(default)]
pub dependencies: Vec<Dependency>,
#[serde(default)]
pub maintainers: Vec<Maintainer>,
pub icon: Option<String>,
#[serde(rename = "appVersion")]
pub app_version: Option<String>,
pub deprecated: Option<bool>,
#[serde(default)]
pub annotations: HashMap<String, String>,
}
impl ChartMetadata {
pub fn has_valid_api_version(&self) -> bool {
matches!(self.api_version, ApiVersion::V1 | ApiVersion::V2)
}
pub fn is_v2(&self) -> bool {
matches!(self.api_version, ApiVersion::V2)
}
pub fn is_library(&self) -> bool {
matches!(self.chart_type, Some(ChartType::Library))
}
pub fn is_deprecated(&self) -> bool {
self.deprecated.unwrap_or(false)
}
pub fn dependency_names(&self) -> Vec<&str> {
self.dependencies.iter().map(|d| d.name.as_str()).collect()
}
pub fn has_duplicate_dependencies(&self) -> Vec<&str> {
let mut seen = std::collections::HashSet::new();
let mut duplicates = Vec::new();
for dep in &self.dependencies {
let name = dep.alias.as_ref().unwrap_or(&dep.name);
if !seen.insert(name.as_str()) {
duplicates.push(name.as_str());
}
}
duplicates
}
}
#[derive(Debug)]
pub struct ChartParseError {
pub message: String,
pub line: Option<u32>,
}
impl std::fmt::Display for ChartParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(line) = self.line {
write!(f, "line {}: {}", line, self.message)
} else {
write!(f, "{}", self.message)
}
}
}
impl std::error::Error for ChartParseError {}
pub fn parse_chart_yaml(content: &str) -> Result<ChartMetadata, ChartParseError> {
serde_yaml::from_str(content).map_err(|e| {
let line = e.location().map(|l| l.line() as u32);
ChartParseError {
message: e.to_string(),
line,
}
})
}
pub fn parse_chart_yaml_file(path: &Path) -> Result<ChartMetadata, ChartParseError> {
let content = std::fs::read_to_string(path).map_err(|e| ChartParseError {
message: format!("Failed to read file: {}", e),
line: None,
})?;
parse_chart_yaml(&content)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_minimal_chart() {
let yaml = r#"
apiVersion: v2
name: test-chart
version: 0.1.0
"#;
let chart = parse_chart_yaml(yaml).unwrap();
assert_eq!(chart.name, "test-chart");
assert_eq!(chart.version, "0.1.0");
assert!(chart.is_v2());
}
#[test]
fn test_parse_full_chart() {
let yaml = r#"
apiVersion: v2
name: my-app
version: 1.2.3
kubeVersion: ">=1.19.0"
description: A sample application
type: application
keywords:
- app
- example
home: https://example.com
sources:
- https://github.com/example/my-app
maintainers:
- name: John Doe
email: john@example.com
icon: https://example.com/icon.png
appVersion: "2.0.0"
dependencies:
- name: postgresql
version: "~11.0"
repository: https://charts.bitnami.com/bitnami
annotations:
category: backend
"#;
let chart = parse_chart_yaml(yaml).unwrap();
assert_eq!(chart.name, "my-app");
assert_eq!(chart.version, "1.2.3");
assert_eq!(chart.kube_version, Some(">=1.19.0".to_string()));
assert_eq!(chart.description, Some("A sample application".to_string()));
assert!(!chart.is_library());
assert_eq!(chart.keywords.len(), 2);
assert_eq!(chart.maintainers.len(), 1);
assert_eq!(chart.dependencies.len(), 1);
}
#[test]
fn test_parse_library_chart() {
let yaml = r#"
apiVersion: v2
name: common
version: 1.0.0
type: library
"#;
let chart = parse_chart_yaml(yaml).unwrap();
assert!(chart.is_library());
}
#[test]
fn test_parse_v1_chart() {
let yaml = r#"
apiVersion: v1
name: legacy-chart
version: 1.0.0
"#;
let chart = parse_chart_yaml(yaml).unwrap();
assert!(!chart.is_v2());
assert!(chart.has_valid_api_version());
}
#[test]
fn test_deprecated_chart() {
let yaml = r#"
apiVersion: v2
name: old-chart
version: 1.0.0
deprecated: true
"#;
let chart = parse_chart_yaml(yaml).unwrap();
assert!(chart.is_deprecated());
}
#[test]
fn test_duplicate_dependencies() {
let yaml = r#"
apiVersion: v2
name: test
version: 1.0.0
dependencies:
- name: redis
version: "1.0.0"
repository: https://charts.bitnami.com/bitnami
- name: redis
version: "2.0.0"
repository: https://charts.bitnami.com/bitnami
"#;
let chart = parse_chart_yaml(yaml).unwrap();
let duplicates = chart.has_duplicate_dependencies();
assert_eq!(duplicates.len(), 1);
assert_eq!(duplicates[0], "redis");
}
#[test]
fn test_parse_error() {
let yaml = "invalid: [yaml";
let result = parse_chart_yaml(yaml);
assert!(result.is_err());
}
}