standarbuild-detect 0.2.0

Detect project kind (Rust, Node, Bun, Deno, Python, Lua, C/C++) and scan polyglot monorepo workspaces
Documentation
//! [`Detector`] trait + [`DetectorRegistry`] for composing project-kind
//! detection. Built-in detectors live in [`crate::builtin`]; downstream
//! crates implement `Detector` for any extra kind they care about.

use std::path::Path;

use crate::kind::KindId;

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct DetectMatch {
	pub kind: KindId,
	pub signals: Vec<String>,
}

pub trait Detector: Send + Sync {
	/// The kind this detector recognises. Used by
	/// [`DetectorRegistry::remove`] and for diagnostics.
	fn kind(&self) -> KindId;

	/// Probe `dir`. Return `Some(DetectMatch)` if the directory looks like a
	/// project of this detector's kind; `None` otherwise.
	fn detect(&self, dir: &Path) -> Option<DetectMatch>;

	/// Higher wins when multiple detectors match the same directory.
	/// Built-in priorities: Rust=100, Bun=80, Deno=70, Node=50, Python=40,
	/// Lua=30, Cpp=20, C=10. Defaults to 0 so plain custom detectors lose to
	/// built-ins unless they declare a higher number explicitly.
	fn priority(&self) -> i32 {
		0
	}
}

pub struct DetectorRegistry {
	detectors: Vec<Box<dyn Detector>>,
}

impl DetectorRegistry {
	pub fn empty() -> Self {
		Self { detectors: Vec::new() }
	}

	pub fn with_builtins() -> Self {
		let mut r = Self::empty();
		crate::builtin::register_all(&mut r);
		r
	}

	pub fn add(&mut self, d: impl Detector + 'static) -> &mut Self {
		self.detectors.push(Box::new(d));
		self
	}

	/// Remove every detector whose `kind()` equals `kind`. Returns `true` if
	/// at least one was removed.
	pub fn remove(&mut self, kind: &KindId) -> bool {
		let before = self.detectors.len();
		self.detectors.retain(|d| d.kind() != *kind);
		self.detectors.len() != before
	}

	pub fn kinds(&self) -> Vec<KindId> {
		self.detectors.iter().map(|d| d.kind()).collect()
	}

	/// Probe every registered detector and return the highest-priority match.
	/// Returns `None` if no detector matched.
	pub fn detect(&self, dir: &Path) -> Option<DetectMatch> {
		if !dir.is_dir() {
			return None;
		}
		let mut best: Option<(i32, DetectMatch)> = None;
		for d in &self.detectors {
			if let Some(m) = d.detect(dir) {
				let p = d.priority();
				if best.as_ref().map_or(true, |(bp, _)| p > *bp) {
					best = Some((p, m));
				}
			}
		}
		best.map(|(_, m)| m)
	}
}

impl Default for DetectorRegistry {
	fn default() -> Self {
		Self::with_builtins()
	}
}