adnv 1.5.0

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

use serde::Deserialize;
use wax::walk::Entry as _;

use super::{Error, Pipe, Result};

/// Creates documentation folder, by copying existing .md file and creating new ones from .yml files.
/// Comments from .yml files will be copied as documentation if they begin with `##`instead of `#`.
#[derive(Debug, Deserialize)]
pub struct Action {
    path: PathBuf,
    /// if true or none, a warning will be display for each folder created without providing README.md file
    show_warnings: Option<bool>,
    /// if true or none, output directory will be dropped before the copy
    clean: Option<bool>,
}

impl Action {
    pub async fn execute(&self, pipe: &mut Pipe) -> Result<()> {
        super::validate_contained_path(&self.path, "document path")?;

        let input = pipe.input().ok_or(Error::NoInputInPipe)?;

        if self.path.exists() && self.clean.unwrap_or(true) {
            tokio::fs::remove_dir_all(&self.path).await.map_err(|e| {
                Error::CouldNotDeleteDirectory {
                    path: self.path.clone(),
                    err: e,
                }
            })?;
        }

        // Documentation creation from fragment files

        let glob = wax::Glob::new("**/*.yml").unwrap();

        for entry in glob.walk(&input) {
            let entry = entry?;
            let mut destination =
                compute_out_path(input.as_path(), self.path.as_path(), entry.path())?;
            build_doc_file(entry.path(), &mut destination).await?;
        }

        // Documentation copy from markdown files

        let glob = wax::Glob::new("**/*.md").unwrap();

        for entry in glob.walk(&input) {
            let entry = entry?;
            let destination = compute_out_path(input.as_path(), self.path.as_path(), entry.path())?;

            // intermediate folders creation
            if let Some(dest) = destination.parent() {
                fs::create_dir_all(dest).map_err(Error::IntermediateDirectory)?;
            }

            fs::copy(entry.path(), &destination).map_err(|e| Error::CouldNotCopyFile {
                source: entry.into_path(),
                destination,
                err: e,
            })?;
        }

        // Compliance warnings

        if self.show_warnings.unwrap_or(true) {
            check_read_me_files(&self.path).map_err(|err| {
                Error::io(
                    err,
                    format!("Error while browsing {} file", self.path.display()),
                )
            })?;
        }

        Ok(())
    }
}

/// Warns if a README.md file is missing in the directory tree
fn check_read_me_files(dir: &Path) -> io::Result<()> {
    if dir.is_dir() {
        for entry in fs::read_dir(dir)? {
            let entry = entry?;
            let path = entry.path();
            if path.is_dir() {
                if !path.join("README.md").exists() {
                    tracing::warn!("path: {} : The README file is missing.", path.display());
                }
                check_read_me_files(&path)?;
            }
        }
    }
    Ok(())
}

/// Creates documentation file from comments in fragment file.
async fn build_doc_file(file: &Path, destination: &mut PathBuf) -> Result<()> {
    let mut buffer = String::new();

    if let Ok(lines) = read_lines(file) {
        for line in lines.map_while(Result::ok) {
            let line = line.trim_start();
            if line.starts_with("##") {
                buffer.push('\n');
                buffer.push_str(line.strip_prefix("##").unwrap().trim_start());
            }
        }
    }

    if !buffer.is_empty() {
        let buffer: String = format!("###### Generated Documentation from adnv ######\n{buffer}");
        destination.set_extension("md");
        create_file(destination.as_path(), &buffer)?;
    }
    Ok(())
}

/// Creates file with content and every intermediate directories if there are
fn create_file(path: &Path, content: &str) -> Result<()> {
    if let Some(dest) = path.parent() {
        fs::create_dir_all(dest).map_err(Error::IntermediateDirectory)?;
    }
    let mut file = File::create(path).map_err(|e| Error::CouldNotOpenFile {
        path: PathBuf::from(path),
        err: e,
    })?;
    file.write_all(content.as_bytes())
        .map_err(|e| Error::CouldNotWriteFile {
            path: PathBuf::from(path),
            err: e,
        })?;
    Ok(())
}

/// Creates a path by removing the `parent` path from the root of the `file` path and concatenating it to the `root` path
fn compute_out_path(parent: &Path, root: &Path, file: &Path) -> Result<PathBuf> {
    let path = file
        .strip_prefix(parent)
        .map_err(|e| Error::CouldNotStripPrefix {
            path: PathBuf::from(file),
            to_strip: PathBuf::from(parent),
            err: e,
        })?;
    Ok(Path::new(root).join(path))
}

fn read_lines(filename: &Path) -> io::Result<io::Lines<io::BufReader<File>>> {
    let file = File::open(filename)?;
    Ok(io::BufReader::new(file).lines())
}