adnv 1.5.0

CLI that tracks, checks and auto-updates infrastructure dependency versions across Terraform, Helm, Docker, Git and npm
Documentation
use std::fmt;
use std::path::{Path, PathBuf};

use smart_default::SmartDefault;

use super::VersionedSource;
use crate::Results;

mod helm;
mod terraform;

#[derive(Debug, Clone)]
pub struct Location {
    path: PathBuf,
    loc: (usize, usize),
    span: Option<(usize, usize)>,
}

impl Location {
    pub fn new(path: impl Into<PathBuf>, line: usize, col: usize) -> Self {
        Self {
            path: path.into(),
            loc: (line, col),
            span: None,
        }
    }

    pub fn with_span(self, start: usize, end: usize) -> Self {
        Self {
            span: Some((start, end)),
            ..self
        }
    }

    pub fn path(&self) -> &Path {
        &self.path
    }

    pub fn loc(&self) -> (usize, usize) {
        self.loc
    }

    pub fn line(&self) -> usize {
        self.loc.0
    }

    pub fn col(&self) -> usize {
        self.loc.1
    }

    pub fn span(&self) -> Option<(usize, usize)> {
        self.span
    }
}

impl PartialEq for Location {
    fn eq(&self, other: &Self) -> bool {
        self.path == other.path && self.loc == other.loc
    }
}

impl Eq for Location {}

impl PartialOrd for Location {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Location {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.path.cmp(&other.path).then(self.loc.cmp(&other.loc))
    }
}

impl fmt::Display for Location {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{file}:{line}",
            file = self.path.display(),
            line = self.line(),
        )
    }
}

#[derive(Debug, Clone, SmartDefault)]
pub struct ParseOptions {
    #[default = false]
    pub recursive: bool,
    #[default = false]
    pub hidden: bool,
}

pub async fn parse_cwd(options: &ParseOptions) -> Results<VersionedSource> {
    let cwd = std::env::current_dir().expect("Should be run in a valid directory");
    parse_dir(&cwd, options).await
}

pub async fn parse_dir(path: impl AsRef<Path>, options: &ParseOptions) -> Results<VersionedSource> {
    use core::iter;

    use itertools::Itertools;

    iter::empty()
        .chain(helm::parse_dir(path.as_ref(), options).await)
        .chain(terraform::parse_dir(path.as_ref(), options).await)
        .partition_result()
        .into()
}

pub fn print_parsing_errors<E>(errors: Vec<E>)
where
    E: miette::Diagnostic + Sync + Send + 'static,
{
    if !errors.is_empty() {
        println!("Following errors are ignored in the next steps.");
        for err in errors {
            println!("{:?}", miette::Report::new(err));
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::sources::{
        VersionedSource, data,
        parse::{self, Location},
    };

    const TEST_DIRECTORY: &str = "tests/sources/parse";

    fn result_non_recursive() -> Vec<VersionedSource> {
        vec![
            VersionedSource::new(data::TerraformSource::new_provider("gitlabhq/gitlab"))
                .with_current("~> 3.8")
                .with_required("~> 3.8")
                .with_location(Location::new("main.sample.test.tf", 4, 17).with_span(100, 108)),
            VersionedSource::new(data::TerraformSource::new_module(
                "gitlab.com/vigigloo/tf-modules/k8slimitednamespace",
            ))
            .with_current("0.3.5")
            .with_required("0.3.5")
            .with_location(Location::new("main.sample.test.tf", 12, 18).with_span(271, 278)),
            VersionedSource::new(data::TerraformSource::new_module(
                "gitlab.com/vigigloo/tf-modules/k8slimitednamespace",
            ))
            .with_location(Location::new("main.sample.test.tf", 17, 18)),
            VersionedSource::new(data::TerraformSource::new_module(
                "gitlab.com/vigigloo/tools-k8s/mattermost",
            ))
            .with_current("0.1.0")
            .with_required("0.1.0")
            .with_location(Location::new("main.sample.test.tf", 21, 13).with_span(477, 484)),
            VersionedSource::new(data::HelmSource::new(
                "https://helm.nginx.com/stable",
                "nginx-ingress",
            ))
            .with_required("> 0.10.0 , < 0.18.0")
            .with_current("0.17.1")
            .with_location(Location::new("main.sample.test.tf", 29, 18).with_span(772, 780)),
            VersionedSource::new(data::TerraformSource::new_module(
                "gitlab.com/vigigloo/tf-modules/k8slimitednamespace",
            ))
            .with_current("2.0.0")
            .with_required("2.0.0")
            .with_location(Location::new("main.sample.test.tf", 33, 18).with_span(951, 958)),
            VersionedSource::new(data::HelmSource::new(
                "https://helm.nginx.com/stable",
                "nginx-ingress",
            ))
            .with_current("0.17.0")
            .with_required("0.17.0")
            .with_location(Location::new("Chart.yaml", 6, 14).with_span(92, 100)),
        ]
    }

    fn result_non_recursive_with_hidden() -> Vec<VersionedSource> {
        let mut vec = result_non_recursive();
        vec.extend(vec![
            VersionedSource::new(data::TerraformSource::new_provider("gitlabhq/gitlab"))
                .with_current("> 3.0")
                .with_required("> 3.0")
                .with_location(Location::new(".hidden.sample.test.tf", 4, 17).with_span(100, 107)),
        ]);
        vec
    }

    fn result_recursive() -> Vec<VersionedSource> {
        let mut vec = result_non_recursive();
        vec.extend(vec![
            VersionedSource::new(data::TerraformSource::new_module(
                "gitlab.com/vigigloo/tf-modules/k8slimitednamespace",
            ))
            .with_current("0.1.0")
            .with_required("0.1.0")
            .with_location(Location::new("subdir/child.sample.test.tf", 2, 18).with_span(118, 125)),
        ]);
        vec
    }

    #[tokio::test]
    async fn parse_succeeds() {
        use std::default::Default;

        let (mut versions, errors): (Vec<_>, Vec<_>) =
            parse::parse_dir(TEST_DIRECTORY, &super::ParseOptions::default())
                .await
                .split();
        let mut wanted = result_non_recursive();

        versions.sort_unstable_by_key(|source| source.source.loc.clone());
        wanted.sort_unstable_by_key(|source| source.source.loc.clone());

        pretty_assertions::assert_eq!(versions, wanted);
        pretty_assertions::assert_eq!(errors.len(), 1);
    }

    #[tokio::test]
    async fn parse_succeeds_with_hidden() {
        let (mut versions, errors): (Vec<_>, Vec<_>) = (parse::parse_dir(
            TEST_DIRECTORY,
            &parse::ParseOptions {
                recursive: false,
                hidden: true,
            },
        ))
        .await
        .split();

        let mut wanted = result_non_recursive_with_hidden();

        versions.sort_unstable_by_key(|source| source.source.loc.clone());
        wanted.sort_unstable_by_key(|source| source.source.loc.clone());

        pretty_assertions::assert_eq!(versions, wanted);
        pretty_assertions::assert_eq!(errors.len(), 1);
    }

    #[tokio::test]
    async fn parse_succeeds_recursive() {
        let (mut versions, errors): (Vec<_>, Vec<_>) = parse::parse_dir(
            TEST_DIRECTORY,
            &parse::ParseOptions {
                recursive: true,
                hidden: false,
            },
        )
        .await
        .split();

        let mut wanted = result_recursive();

        versions.sort_unstable_by_key(|source| source.source.loc.clone());
        wanted.sort_unstable_by_key(|source| source.source.loc.clone());

        pretty_assertions::assert_eq!(versions, wanted);
        pretty_assertions::assert_eq!(errors.len(), 1);
    }
}