makers 0.8.0

a POSIX-compatible make implemented in Rust
use std::collections::HashMap;
use std::{fmt, mem};

use super::pattern::r#match;
use super::{CommandLine, ItemSource, MacroSet};
use eyre::{eyre, OptionExt, Result};
use regex::Captures;

#[derive(Clone, Debug)]
pub struct InferenceRule {
    pub source: ItemSource,
    pub products: Vec<String>,
    pub prerequisites: Vec<String>,
    pub commands: Vec<CommandLine>,
    pub macros: MacroSet,
}

impl InferenceRule {
    /// s1 is the product, s2 is the prereq
    pub fn new_suffix(
        source: ItemSource,
        s1: String,
        s2: String,
        commands: Vec<CommandLine>,
        macros: MacroSet,
    ) -> Self {
        Self {
            source,
            products: vec![format!("%{}", s1)],
            prerequisites: vec![format!("%{}", s2)],
            commands,
            macros,
        }
    }

    pub fn first_match<'s, 't: 's>(&'s self, target_name: &'t str) -> Result<Option<Captures<'t>>> {
        self.products
            .iter()
            .map(|pattern| {
                // TODO find a better way to make the self_subdir_match test pass
                r#match(pattern.strip_prefix("./").unwrap_or(pattern), target_name)
            })
            .try_fold(None, |x, y| y.map(|y| x.or(y)))
    }

    pub fn matches(&self, target_name: &str) -> Result<bool> {
        self.first_match(target_name).map(|x| x.is_some())
    }

    pub fn prereqs<'s>(
        &'s self,
        target_name: &'s str,
    ) -> Result<impl Iterator<Item = String> + 's> {
        let capture = self
            .first_match(target_name)?
            .ok_or_else(|| eyre!("asked non-matching inference rule for prerequisites"))?;
        let percent_expansion = capture
            .get(1)
            .ok_or_eyre("should've matched the %")?
            .as_str();
        Ok(self
            .prerequisites
            .iter()
            .map(move |p| p.replace('%', percent_expansion)))
    }

    fn extend(&mut self, other: Self) {
        assert_eq!(&self.products, &other.products);
        match (self.commands.is_empty(), other.commands.is_empty()) {
            (false, false) => {
                // both rules have commands, so replace this entirely
                *self = other;
            }
            (true, false) => {
                // this rule doesn't have commands, but the other one does,
                // so it's the real one
                let mut other = other;
                mem::swap(self, &mut other);
                self.extend(other);
            }
            (false, true) | (true, true) => {
                // this rule might have commands, but the other one doesn't,
                // so append non-command stuff
                // TODO decide something smart about sources
                self.prerequisites.extend(other.prerequisites);
                self.macros.extend(other.macros);
            }
        }
    }
}

impl fmt::Display for InferenceRule {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(
            f,
            "{}: {}",
            self.products.join(" "),
            self.prerequisites.join(" ")
        )?;
        for command in &self.commands {
            writeln!(f, "\t{}", command)?;
        }
        Ok(())
    }
}

#[derive(Clone, Default)]
pub struct InferenceRuleSet {
    /// Maps from products to a map from prerequisites to rules.
    data: HashMap<Vec<String>, HashMap<Vec<String>, InferenceRule>>,
}

impl InferenceRuleSet {
    pub fn get(&self, products: &[String], prerequisites: &[String]) -> Option<&InferenceRule> {
        self.data.get(products).and_then(|x| x.get(prerequisites))
    }

    fn get_mut(
        &mut self,
        products: &[String],
        prerequisites: &[String],
    ) -> Option<&mut InferenceRule> {
        self.data
            .get_mut(products)
            .and_then(|x| x.get_mut(prerequisites))
    }

    pub fn put(&mut self, rule: InferenceRule) {
        if let Some(existing_rule) = self.get_mut(&rule.products, &rule.prerequisites) {
            existing_rule.extend(rule);
        } else {
            self.data
                .entry(rule.products.clone())
                .or_default()
                .insert(rule.prerequisites.clone(), rule);
        }
    }

    pub fn len(&self) -> usize {
        self.data.len()
    }

    pub fn extend(&mut self, other: Self) {
        for other in other.data.into_values().flat_map(HashMap::into_values) {
            self.put(other);
        }
    }

    pub fn iter(&self) -> impl Iterator<Item = &InferenceRule> {
        self.data.values().flat_map(HashMap::values)
    }
}

impl From<Vec<InferenceRule>> for InferenceRuleSet {
    fn from(value: Vec<InferenceRule>) -> Self {
        let mut result = Self::default();
        for rule in value {
            result.put(rule);
        }
        result
    }
}

#[cfg(test)]
mod test {
    use super::*;

    type R = Result<()>;

    #[test]
    fn suffix_match() -> R {
        let rule = InferenceRule::new_suffix(
            ItemSource::Builtin,
            ".o".to_owned(),
            ".c".to_owned(),
            vec![],
            MacroSet::new(),
        );
        assert!(rule.matches("foo.o")?);
        assert!(rule.matches("dir/foo.o")?);
        Ok(())
    }

    #[cfg(feature = "full")]
    #[test]
    fn percent_match() -> R {
        // thanks, SPDX License List
        let rule = InferenceRule {
            source: ItemSource::Builtin,
            products: vec!["licenseListPublisher-%.jar-valid".to_owned()],
            prerequisites: vec![
                "licenseListPublisher-%.jar.asc".to_owned(),
                "licenseListPublisher-%.jar".to_owned(),
                "goneall.gpg".to_owned(),
            ],
            commands: vec![],
            macros: MacroSet::new(),
        };
        assert!(rule.matches("licenseListPublisher-2.2.1.jar-valid")?);
        Ok(())
    }

    #[cfg(feature = "full")]
    #[test]
    fn subdir_match() -> R {
        let rule = InferenceRule {
            source: ItemSource::Builtin,
            products: vec!["a/%.o".to_owned()],
            prerequisites: vec!["a/%.c".to_owned()],
            commands: vec![],
            macros: MacroSet::new(),
        };
        assert!(rule.matches("a/foo.o")?);
        Ok(())
    }

    #[cfg(feature = "full")]
    #[test]
    fn self_subdir_match() -> R {
        let rule = InferenceRule {
            source: ItemSource::Builtin,
            products: vec!["./%.o".to_owned()],
            prerequisites: vec!["./%.c".to_owned()],
            commands: vec![],
            macros: MacroSet::new(),
        };
        assert!(rule.matches("foo.o")?);
        Ok(())
    }
}