use std::{fmt, path::Path};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Ecosystem {
Node,
Rust,
Python,
Elixir,
Java,
}
impl fmt::Display for Ecosystem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Node => write!(f, "node"),
Self::Rust => write!(f, "rust"),
Self::Python => write!(f, "python"),
Self::Elixir => write!(f, "elixir"),
Self::Java => write!(f, "java"),
}
}
}
impl Ecosystem {
pub const ALL: [Self; 5] = [
Self::Node,
Self::Rust,
Self::Python,
Self::Elixir,
Self::Java,
];
}
impl std::str::FromStr for Ecosystem {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"node" | "nodejs" | "js" | "javascript" => Ok(Self::Node),
"rust" | "rs" => Ok(Self::Rust),
"python" | "py" => Ok(Self::Python),
"elixir" | "ex" => Ok(Self::Elixir),
"java" | "jvm" | "scala" | "kotlin" | "kt" | "sbt" => Ok(Self::Java),
_ => Err(format!("unknown ecosystem: {s}")),
}
}
}
fn detect_from_command(command: &str) -> Option<Ecosystem> {
let basename = Path::new(command)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(command);
match basename {
"node" | "npm" | "npx" | "yarn" | "pnpm" | "bun" => Some(Ecosystem::Node),
"cargo" | "rustc" | "rustup" => Some(Ecosystem::Rust),
"python" | "python3" | "pip" | "pip3" | "uv" | "poetry" | "pdm" | "rye" => {
Some(Ecosystem::Python)
}
"mix" | "elixir" | "iex" => Some(Ecosystem::Elixir),
"java" | "javac" | "mvn" | "mvnw" | "gradle" | "gradlew" | "sbt" | "scala" | "scalac"
| "kotlinc" => Some(Ecosystem::Java),
_ => None,
}
}
fn detect_from_files(pwd: &Path) -> Option<Ecosystem> {
let markers: &[(&str, Ecosystem)] = &[
("Cargo.toml", Ecosystem::Rust),
("mix.exs", Ecosystem::Elixir),
("package.json", Ecosystem::Node),
("pyproject.toml", Ecosystem::Python),
("setup.py", Ecosystem::Python),
("requirements.txt", Ecosystem::Python),
("Pipfile", Ecosystem::Python),
("pom.xml", Ecosystem::Java),
("build.gradle", Ecosystem::Java),
("build.gradle.kts", Ecosystem::Java),
("build.sbt", Ecosystem::Java),
];
for (marker, ecosystem) in markers {
if pwd.join(marker).exists() {
return Some(*ecosystem);
}
}
None
}
pub fn detect(command: &str, pwd: &Path) -> Option<Ecosystem> {
detect_from_command(command).or_else(|| detect_from_files(pwd))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_detect_node_from_npm() {
assert_eq!(detect_from_command("npm"), Some(Ecosystem::Node));
assert_eq!(detect_from_command("yarn"), Some(Ecosystem::Node));
assert_eq!(detect_from_command("pnpm"), Some(Ecosystem::Node));
assert_eq!(detect_from_command("bun"), Some(Ecosystem::Node));
}
#[test]
fn test_should_detect_rust_from_cargo() {
assert_eq!(detect_from_command("cargo"), Some(Ecosystem::Rust));
}
#[test]
fn test_should_detect_python_from_pip() {
assert_eq!(detect_from_command("pip"), Some(Ecosystem::Python));
assert_eq!(detect_from_command("uv"), Some(Ecosystem::Python));
assert_eq!(detect_from_command("poetry"), Some(Ecosystem::Python));
}
#[test]
fn test_should_detect_elixir_from_mix() {
assert_eq!(detect_from_command("mix"), Some(Ecosystem::Elixir));
}
#[test]
fn test_should_detect_java_from_gradle() {
assert_eq!(detect_from_command("gradle"), Some(Ecosystem::Java));
assert_eq!(detect_from_command("gradlew"), Some(Ecosystem::Java));
assert_eq!(detect_from_command("mvn"), Some(Ecosystem::Java));
assert_eq!(detect_from_command("mvnw"), Some(Ecosystem::Java));
assert_eq!(detect_from_command("sbt"), Some(Ecosystem::Java));
assert_eq!(detect_from_command("scala"), Some(Ecosystem::Java));
assert_eq!(detect_from_command("kotlinc"), Some(Ecosystem::Java));
}
#[test]
fn test_should_return_none_for_unknown() {
assert_eq!(detect_from_command("unknown-tool"), None);
}
#[test]
fn test_should_detect_from_full_path() {
assert_eq!(
detect_from_command("/usr/local/bin/npm"),
Some(Ecosystem::Node)
);
}
#[test]
fn test_should_parse_ecosystem_from_str() {
assert_eq!("node".parse::<Ecosystem>(), Ok(Ecosystem::Node));
assert_eq!("js".parse::<Ecosystem>(), Ok(Ecosystem::Node));
assert_eq!("rust".parse::<Ecosystem>(), Ok(Ecosystem::Rust));
assert_eq!("py".parse::<Ecosystem>(), Ok(Ecosystem::Python));
assert_eq!("ex".parse::<Ecosystem>(), Ok(Ecosystem::Elixir));
assert_eq!("jvm".parse::<Ecosystem>(), Ok(Ecosystem::Java));
assert_eq!("scala".parse::<Ecosystem>(), Ok(Ecosystem::Java));
assert_eq!("kotlin".parse::<Ecosystem>(), Ok(Ecosystem::Java));
assert_eq!("sbt".parse::<Ecosystem>(), Ok(Ecosystem::Java));
assert!("unknown".parse::<Ecosystem>().is_err());
}
}