miho 8.2.3

Repository management tools
use crate::agent::Agent;
use crate::package::Package;
use crate::package::manifest::ManifestKind;
use anyhow::{Result, bail};
use globset::{Glob, GlobSet, GlobSetBuilder};
use ignore::{DirEntry, WalkBuilder};
use itertools::Itertools;
use std::path::Path;
use strum::IntoEnumIterator;
use tap::TapFallible;

#[derive(Debug)]
pub struct SearchBuilder<'a> {
  path: Vec<&'a Path>,
  packages: Vec<&'a str>,
  agents: Vec<Agent>,
}

impl<'a> SearchBuilder<'a> {
  pub fn new(path: &[&'a Path]) -> Self {
    Self {
      path: path.to_vec(),
      packages: Vec::new(),
      agents: Vec::new(),
    }
  }

  pub fn package(mut self, name: &[&'a str]) -> Self {
    self.packages.extend(name);
    self
  }

  pub fn agent(mut self, agents: &[&str]) -> Self {
    let agents = agents
      .iter()
      .map(|it| it.to_uppercase())
      .filter_map(|it| Agent::try_from(it.as_str()).ok())
      .unique();

    self.agents.extend(agents);
    self
  }

  pub fn search(self) -> Result<Vec<Package>> {
    let Some((first, other)) = self.path.split_first() else {
      return Ok(Vec::new());
    };

    let mut walker = WalkBuilder::new(first);
    for path in other {
      walker.add(path);
    }

    let glob = build_globset()?;
    let mut packages = Vec::new();

    for entry in walker.build().flatten() {
      if is_match(&glob, &entry) {
        let path = entry.path().canonicalize()?;
        let package = Package::new(path).tap_err_dbg(|err| eprintln!("{err}"));

        if matches!(package, Ok(ref it) if !packages.contains(it)) {
          packages.push(unsafe { package.unwrap_unchecked() });
        }
      }
    }

    if !self.packages.is_empty() {
      packages.retain(|it| self.packages.contains(&it.name.as_str()));
    }

    if !self.agents.is_empty() {
      packages.retain(|it| self.agents.contains(&it.manifest.agent()));
    }

    if packages.is_empty() {
      bail!("no valid package found");
    }

    packages.sort();

    Ok(packages)
  }
}

fn build_globset() -> Result<GlobSet> {
  let mut builder = GlobSetBuilder::new();

  for kind in ManifestKind::iter() {
    builder.add(Glob::new(kind.glob())?);
  }

  Ok(builder.build()?)
}

fn is_match(glob: &GlobSet, entry: &DirEntry) -> bool {
  if !glob.is_match(entry.path()) {
    return false;
  }

  matches!(entry.file_type(), Some(it) if !it.is_dir())
}