use std::collections::{HashMap, HashSet};
use std::path::Path;
use serde_yaml::Value;
#[derive(Debug, Clone)]
pub struct ValuesFile {
pub values: Value,
pub line_map: HashMap<String, u32>,
pub defined_paths: HashSet<String>,
}
impl ValuesFile {
pub fn empty() -> Self {
Self {
values: Value::Mapping(serde_yaml::Mapping::new()),
line_map: HashMap::new(),
defined_paths: HashSet::new(),
}
}
pub fn get(&self, path: &str) -> Option<&Value> {
let parts: Vec<&str> = path.split('.').collect();
let mut current = &self.values;
for part in parts {
match current {
Value::Mapping(map) => {
current = map.get(Value::String(part.to_string()))?;
}
_ => return None,
}
}
Some(current)
}
pub fn has_path(&self, path: &str) -> bool {
self.defined_paths.contains(path)
}
pub fn line_for_path(&self, path: &str) -> Option<u32> {
self.line_map.get(path).copied()
}
pub fn paths_with_prefix(&self, prefix: &str) -> Vec<&str> {
self.defined_paths
.iter()
.filter(|p| p.starts_with(prefix))
.map(|s| s.as_str())
.collect()
}
pub fn is_sensitive_path(path: &str) -> bool {
let lower = path.to_lowercase();
lower.contains("password")
|| lower.contains("secret")
|| lower.contains("token")
|| lower.contains("key")
|| lower.contains("credential")
|| lower.contains("apikey")
|| lower.contains("api_key")
|| lower.ends_with(".auth")
}
pub fn sensitive_paths(&self) -> Vec<&str> {
self.defined_paths
.iter()
.filter(|p| Self::is_sensitive_path(p))
.map(|s| s.as_str())
.collect()
}
}
#[derive(Debug)]
pub struct ValuesParseError {
pub message: String,
pub line: Option<u32>,
}
impl std::fmt::Display for ValuesParseError {
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 ValuesParseError {}
pub fn parse_values_yaml(content: &str) -> Result<ValuesFile, ValuesParseError> {
let values: Value = serde_yaml::from_str(content).map_err(|e| {
let line = e.location().map(|l| l.line() as u32);
ValuesParseError {
message: e.to_string(),
line,
}
})?;
let (line_map, defined_paths) = build_line_map(content);
Ok(ValuesFile {
values,
line_map,
defined_paths,
})
}
pub fn parse_values_yaml_file(path: &Path) -> Result<ValuesFile, ValuesParseError> {
let content = std::fs::read_to_string(path).map_err(|e| ValuesParseError {
message: format!("Failed to read file: {}", e),
line: None,
})?;
parse_values_yaml(&content)
}
fn build_line_map(content: &str) -> (HashMap<String, u32>, HashSet<String>) {
let mut line_map = HashMap::new();
let mut defined_paths = HashSet::new();
let mut path_stack: Vec<(String, usize)> = Vec::new();
for (line_num, line) in content.lines().enumerate() {
let line_number = (line_num + 1) as u32;
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let indent = line.len() - line.trim_start().len();
while let Some((_, stack_indent)) = path_stack.last() {
if indent <= *stack_indent {
path_stack.pop();
} else {
break;
}
}
if let Some(colon_pos) = trimmed.find(':') {
let key = trimmed[..colon_pos].trim();
if key.contains(' ') && !key.starts_with('"') && !key.starts_with('\'') {
continue;
}
let key = key.trim_matches('"').trim_matches('\'');
let full_path = if path_stack.is_empty() {
key.to_string()
} else {
let parent_path = &path_stack.last().unwrap().0;
format!("{}.{}", parent_path, key)
};
line_map.insert(full_path.clone(), line_number);
defined_paths.insert(full_path.clone());
let after_colon = trimmed[colon_pos + 1..].trim();
if after_colon.is_empty() || after_colon.starts_with('#') {
path_stack.push((full_path, indent));
}
}
}
(line_map, defined_paths)
}
pub fn extract_values_path(expr: &str) -> Option<&str> {
let trimmed = expr.trim();
trimmed.strip_prefix(".Values.")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_values() {
let yaml = r#"
replicaCount: 1
image:
repository: nginx
tag: "1.25"
"#;
let values = parse_values_yaml(yaml).unwrap();
assert!(values.has_path("replicaCount"));
assert!(values.has_path("image"));
assert!(values.has_path("image.repository"));
assert!(values.has_path("image.tag"));
}
#[test]
fn test_get_value() {
let yaml = r#"
image:
repository: nginx
tag: "1.25"
service:
port: 80
"#;
let values = parse_values_yaml(yaml).unwrap();
assert_eq!(
values.get("image.repository"),
Some(&Value::String("nginx".to_string()))
);
assert_eq!(values.get("service.port"), Some(&Value::Number(80.into())));
assert_eq!(values.get("nonexistent"), None);
}
#[test]
fn test_line_numbers() {
let yaml = r#"replicaCount: 1
image:
repository: nginx
tag: "1.25"
"#;
let values = parse_values_yaml(yaml).unwrap();
assert_eq!(values.line_for_path("replicaCount"), Some(1));
assert_eq!(values.line_for_path("image"), Some(2));
assert_eq!(values.line_for_path("image.repository"), Some(3));
assert_eq!(values.line_for_path("image.tag"), Some(4));
}
#[test]
fn test_sensitive_paths() {
let yaml = r#"
database:
password: secret123
host: localhost
auth:
apiKey: abc123
token: xyz789
"#;
let values = parse_values_yaml(yaml).unwrap();
let sensitive = values.sensitive_paths();
assert!(sensitive.contains(&"database.password"));
assert!(sensitive.contains(&"auth.apiKey"));
assert!(sensitive.contains(&"auth.token"));
assert!(!sensitive.contains(&"database.host"));
}
#[test]
fn test_extract_values_path() {
assert_eq!(
extract_values_path(".Values.image.repository"),
Some("image.repository")
);
assert_eq!(
extract_values_path(".Values.replicaCount"),
Some("replicaCount")
);
assert_eq!(extract_values_path(".Release.Name"), None);
assert_eq!(extract_values_path("something.else"), None);
}
#[test]
fn test_paths_with_prefix() {
let yaml = r#"
image:
repository: nginx
tag: "1.25"
pullPolicy: Always
service:
port: 80
"#;
let values = parse_values_yaml(yaml).unwrap();
let image_paths = values.paths_with_prefix("image.");
assert_eq!(image_paths.len(), 3);
assert!(image_paths.contains(&"image.repository"));
assert!(image_paths.contains(&"image.tag"));
assert!(image_paths.contains(&"image.pullPolicy"));
}
#[test]
fn test_empty_values() {
let values = ValuesFile::empty();
assert!(!values.has_path("anything"));
}
#[test]
fn test_parse_error() {
let yaml = "invalid: [yaml";
let result = parse_values_yaml(yaml);
assert!(result.is_err());
}
}