use std::path::Path;
use wax::walk::Entry as _;
use super::ParseOptions;
use crate::sources::VersionedSource;
use crate::{Error, Result};
pub async fn parse_dir(
path: impl AsRef<Path>,
opts: &ParseOptions,
) -> impl IntoIterator<Item = Result<VersionedSource>> {
use futures::future::join_all;
use itertools::Itertools;
let pattern = if opts.recursive {
"**/Chart.yaml"
} else {
"Chart.yaml"
};
let glob = wax::Glob::new(pattern).unwrap();
let walker = glob
.walk(path.as_ref())
.filter_map(std::result::Result::ok)
.filter(|entry| opts.hidden || !entry.to_candidate_path().as_ref().starts_with('.'))
.map(parse_chart);
join_all(walker).await.into_iter().flatten_ok()
}
async fn parse_chart(walk_entry: wax::walk::GlobEntry) -> Result<Vec<VersionedSource>> {
let mut path = walk_entry.to_candidate_path().to_string();
if path.is_empty() {
path = "Chart.yaml".to_string();
}
let chart_content = tokio::fs::read_to_string(walk_entry.path())
.await
.map_err(|err| Error::io(err, "Failed to read helm chart"))?;
let root_node = marked_yaml::parse_yaml(0, &chart_content)?;
let Some(root) = root_node.as_mapping() else {
tracing::warn!(
"Ignoring `{}`: a chart must be a YAML mapping",
walk_entry.path().display()
);
return Ok(vec![]);
};
let Some(deps) = root.get_sequence("dependencies") else {
return Ok(vec![]);
};
Ok(deps
.iter()
.filter_map(|entry| {
use super::Location;
use crate::sources::data::HelmSource;
use crate::util::parse;
let entry = entry.as_mapping()?;
let version_node = entry.get_scalar("version")?;
let version_start = version_node.span().start()?;
let version_start_idx =
parse::loc_to_index(&chart_content, version_start.line(), version_start.column())?;
let mut version_end_idx = version_start_idx + version_node.len();
if chart_content.as_bytes().get(version_start_idx) == Some(&b'"') {
version_end_idx += 2;
}
let name = entry.get_scalar("name")?.as_str();
let version = version_node.as_str();
let repository = entry
.get_scalar("repository")
.map(marked_yaml::types::MarkedScalarNode::as_str)?;
let loc = Location::new(path.clone(), version_start.line(), version_start.column())
.with_span(version_start_idx, version_end_idx);
let source = HelmSource {
repository: repository.to_string(),
name: name.to_string(),
};
let versioned_source = VersionedSource::new(source)
.with_current(version.to_owned())
.with_required(version.to_owned());
Some(versioned_source.with_location(loc))
})
.collect())
}
#[cfg(test)]
mod tests {
use itertools::Itertools;
use super::parse_dir;
use crate::sources::data::{HelmSource, Source as SourceData};
use crate::sources::parse::Location;
use crate::sources::{VersionedSource, parse::ParseOptions};
const TEST_DIRECTORY: &str = "tests/sources/parse";
#[tokio::test]
async fn parse_chart() {
let (mut versions, errors): (Vec<_>, Vec<_>) = (parse_dir(
TEST_DIRECTORY,
&ParseOptions {
recursive: false,
hidden: true,
},
))
.await
.into_iter()
.partition_result();
let expected = vec![
VersionedSource::new(SourceData::Helm(HelmSource {
repository: "https://helm.nginx.com/stable".to_string(),
name: "nginx-ingress".to_string(),
}))
.with_current("0.17.0")
.with_required("0.17.0")
.with_location(Location::new("Chart.yaml", 6, 14).with_span(92, 100)),
];
versions.sort_unstable();
assert_eq!(versions, expected);
assert!(errors.is_empty());
}
}