dnf-repofile 0.1.0

Pure Rust library for parsing, managing, validating, diffing, and rendering DNF/YUM .repo configuration files
Documentation
use crate::error::Result;
use crate::repo::Repo;
use crate::repofile::RepoFile;
use crate::types::RepoId;
use crate::validate::{IssueLevel, IssueLocation, ValidationIssue, ValidationReport};
use indexmap::IndexMap;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

/// Manages a directory of `.repo` files on disk
#[derive(Debug)]
pub struct ReposDir {
    path: PathBuf,
    files: IndexMap<String, RepoFile>,
}

impl ReposDir {
    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
        let path = path.as_ref().to_path_buf();
        let mut files = IndexMap::new();

        if path.is_dir() {
            let mut entries: Vec<_> = fs::read_dir(&path)?
                .filter_map(|e| e.ok())
                .filter(|e| {
                    e.file_name()
                        .to_str()
                        .map(|n| n.ends_with(".repo"))
                        .unwrap_or(false)
                })
                .collect();
            entries.sort_by_key(|e| e.file_name());

            for entry in entries {
                let name = entry.file_name().to_string_lossy().to_string();
                if let Ok(contents) = fs::read_to_string(entry.path()) {
                    if let Ok(rf) = RepoFile::parse(&contents) {
                        files.insert(name, rf);
                    }
                }
            }
        }

        Ok(ReposDir { path, files })
    }

    pub fn save_all(&self) -> std::result::Result<(), Vec<(String, std::io::Error)>> {
        let mut errs = Vec::new();
        for (name, rf) in &self.files {
            if let Err(e) = fs::write(self.path.join(name), rf.render()) {
                errs.push((name.clone(), e));
            }
        }
        if errs.is_empty() {
            Ok(())
        } else {
            Err(errs)
        }
    }

    pub fn save(&self, filename: &str) -> std::result::Result<(), std::io::Error> {
        match self.files.get(filename) {
            Some(rf) => fs::write(self.path.join(filename), rf.render()),
            None => Err(std::io::Error::new(
                std::io::ErrorKind::NotFound,
                "file not found",
            )),
        }
    }

    pub fn file_names(&self) -> Vec<&str> {
        self.files.keys().map(|s| s.as_str()).collect()
    }

    pub fn get_file(&self, filename: &str) -> Option<&RepoFile> {
        self.files.get(filename)
    }

    pub fn get_file_mut(&mut self, filename: &str) -> Option<&mut RepoFile> {
        self.files.get_mut(filename)
    }

    pub fn set_file(&mut self, filename: &str, file: RepoFile) {
        self.files.insert(filename.to_string(), file);
    }

    pub fn remove_file(
        &mut self,
        filename: &str,
    ) -> std::result::Result<Option<RepoFile>, std::io::Error> {
        let removed = self.files.shift_remove(filename);
        let fp = self.path.join(filename);
        if fp.exists() {
            fs::remove_file(fp)?;
        }
        Ok(removed)
    }

    pub fn create_file(&mut self, filename: &str) -> &mut RepoFile {
        self.files.entry(filename.to_string()).or_default()
    }

    pub fn find_repo(&self, id: &RepoId) -> Option<(&str, &Repo)> {
        for (name, rf) in &self.files {
            if let Some(block) = rf.get(id) {
                return Some((name.as_str(), &block.data));
            }
        }
        None
    }

    pub fn file_for_repo(&self, id: &RepoId) -> Option<&str> {
        for (name, rf) in &self.files {
            if rf.contains(id) {
                return Some(name.as_str());
            }
        }
        None
    }

    pub fn all_repos(&self) -> Vec<(&str, &Repo)> {
        let mut r = Vec::new();
        for (name, rf) in &self.files {
            for (_, block) in rf.iter() {
                r.push((name.as_str(), &block.data));
            }
        }
        r
    }

    pub fn repo_count(&self) -> usize {
        self.files.values().map(|rf| rf.len()).sum()
    }

    pub fn iter_repos(&self) -> impl Iterator<Item = (&str, &Repo)> {
        self.files.iter().flat_map(|(name, rf)| {
            rf.iter()
                .map(move |(_, block)| (name.as_str(), &block.data))
        })
    }

    #[must_use]
    pub fn validate(&self) -> ValidationReport {
        let mut report = ValidationReport::new();
        let mut seen: HashMap<&RepoId, &str> = HashMap::new();

        for (fname, rf) in &self.files {
            for (repo_id, block) in rf.iter() {
                if let Some(existing) = seen.get(repo_id) {
                    report.errors.push(ValidationIssue {
                        level: IssueLevel::Error,
                        location: IssueLocation::File(fname.clone()),
                        field: None,
                        message: format!(
                            "duplicate repo ID '{}' already defined in '{}'",
                            repo_id.as_ref(),
                            existing
                        ),
                    });
                } else {
                    seen.insert(repo_id, fname.as_str());
                }
                let rr = block.data.validate();
                report.errors.extend(rr.errors);
                report.warnings.extend(rr.warnings);
            }
        }

        report
    }
}