Skip to main content

sbe_core/
detect.rs

1use std::{fmt, path::Path};
2
3use serde::{Deserialize, Serialize};
4
5/// Supported language ecosystems.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub enum Ecosystem {
9    Node,
10    Rust,
11    Python,
12    Elixir,
13    Java,
14}
15
16impl fmt::Display for Ecosystem {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            Self::Node => write!(f, "node"),
20            Self::Rust => write!(f, "rust"),
21            Self::Python => write!(f, "python"),
22            Self::Elixir => write!(f, "elixir"),
23            Self::Java => write!(f, "java"),
24        }
25    }
26}
27
28impl Ecosystem {
29    /// All known ecosystems.
30    pub const ALL: [Self; 5] = [
31        Self::Node,
32        Self::Rust,
33        Self::Python,
34        Self::Elixir,
35        Self::Java,
36    ];
37}
38
39impl std::str::FromStr for Ecosystem {
40    type Err = String;
41
42    fn from_str(s: &str) -> Result<Self, Self::Err> {
43        match s.to_lowercase().as_str() {
44            "node" | "nodejs" | "js" | "javascript" => Ok(Self::Node),
45            "rust" | "rs" => Ok(Self::Rust),
46            "python" | "py" => Ok(Self::Python),
47            "elixir" | "ex" => Ok(Self::Elixir),
48            "java" | "jvm" | "scala" | "kotlin" | "kt" | "sbt" => Ok(Self::Java),
49            _ => Err(format!("unknown ecosystem: {s}")),
50        }
51    }
52}
53
54/// Detect the ecosystem from the command being run.
55fn detect_from_command(command: &str) -> Option<Ecosystem> {
56    // Extract basename from the command (handles full paths)
57    let basename = Path::new(command)
58        .file_name()
59        .and_then(|n| n.to_str())
60        .unwrap_or(command);
61
62    match basename {
63        // Node.js
64        "node" | "npm" | "npx" | "yarn" | "pnpm" | "bun" => Some(Ecosystem::Node),
65        // Rust
66        "cargo" | "rustc" | "rustup" => Some(Ecosystem::Rust),
67        // Python
68        "python" | "python3" | "pip" | "pip3" | "uv" | "poetry" | "pdm" | "rye" => {
69            Some(Ecosystem::Python)
70        }
71        // Elixir
72        "mix" | "elixir" | "iex" => Some(Ecosystem::Elixir),
73        // Java / Scala / Kotlin
74        "java" | "javac" | "mvn" | "mvnw" | "gradle" | "gradlew" | "sbt" | "scala" | "scalac"
75        | "kotlinc" => Some(Ecosystem::Java),
76        _ => None,
77    }
78}
79
80/// Detect the ecosystem from marker files in the working directory.
81fn detect_from_files(pwd: &Path) -> Option<Ecosystem> {
82    // Check in priority order — more specific markers first
83    let markers: &[(&str, Ecosystem)] = &[
84        ("Cargo.toml", Ecosystem::Rust),
85        ("mix.exs", Ecosystem::Elixir),
86        ("package.json", Ecosystem::Node),
87        ("pyproject.toml", Ecosystem::Python),
88        ("setup.py", Ecosystem::Python),
89        ("requirements.txt", Ecosystem::Python),
90        ("Pipfile", Ecosystem::Python),
91        ("pom.xml", Ecosystem::Java),
92        ("build.gradle", Ecosystem::Java),
93        ("build.gradle.kts", Ecosystem::Java),
94        ("build.sbt", Ecosystem::Java),
95    ];
96
97    for (marker, ecosystem) in markers {
98        if pwd.join(marker).exists() {
99            return Some(*ecosystem);
100        }
101    }
102    None
103}
104
105/// Detect the ecosystem from command name and working directory.
106///
107/// Returns `None` if no ecosystem could be determined.
108pub fn detect(command: &str, pwd: &Path) -> Option<Ecosystem> {
109    detect_from_command(command).or_else(|| detect_from_files(pwd))
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_should_detect_node_from_npm() {
118        assert_eq!(detect_from_command("npm"), Some(Ecosystem::Node));
119        assert_eq!(detect_from_command("yarn"), Some(Ecosystem::Node));
120        assert_eq!(detect_from_command("pnpm"), Some(Ecosystem::Node));
121        assert_eq!(detect_from_command("bun"), Some(Ecosystem::Node));
122    }
123
124    #[test]
125    fn test_should_detect_rust_from_cargo() {
126        assert_eq!(detect_from_command("cargo"), Some(Ecosystem::Rust));
127    }
128
129    #[test]
130    fn test_should_detect_python_from_pip() {
131        assert_eq!(detect_from_command("pip"), Some(Ecosystem::Python));
132        assert_eq!(detect_from_command("uv"), Some(Ecosystem::Python));
133        assert_eq!(detect_from_command("poetry"), Some(Ecosystem::Python));
134    }
135
136    #[test]
137    fn test_should_detect_elixir_from_mix() {
138        assert_eq!(detect_from_command("mix"), Some(Ecosystem::Elixir));
139    }
140
141    #[test]
142    fn test_should_detect_java_from_gradle() {
143        assert_eq!(detect_from_command("gradle"), Some(Ecosystem::Java));
144        assert_eq!(detect_from_command("gradlew"), Some(Ecosystem::Java));
145        assert_eq!(detect_from_command("mvn"), Some(Ecosystem::Java));
146        assert_eq!(detect_from_command("mvnw"), Some(Ecosystem::Java));
147        assert_eq!(detect_from_command("sbt"), Some(Ecosystem::Java));
148        assert_eq!(detect_from_command("scala"), Some(Ecosystem::Java));
149        assert_eq!(detect_from_command("kotlinc"), Some(Ecosystem::Java));
150    }
151
152    #[test]
153    fn test_should_return_none_for_unknown() {
154        assert_eq!(detect_from_command("unknown-tool"), None);
155    }
156
157    #[test]
158    fn test_should_detect_from_full_path() {
159        assert_eq!(
160            detect_from_command("/usr/local/bin/npm"),
161            Some(Ecosystem::Node)
162        );
163    }
164
165    #[test]
166    fn test_should_parse_ecosystem_from_str() {
167        assert_eq!("node".parse::<Ecosystem>(), Ok(Ecosystem::Node));
168        assert_eq!("js".parse::<Ecosystem>(), Ok(Ecosystem::Node));
169        assert_eq!("rust".parse::<Ecosystem>(), Ok(Ecosystem::Rust));
170        assert_eq!("py".parse::<Ecosystem>(), Ok(Ecosystem::Python));
171        assert_eq!("ex".parse::<Ecosystem>(), Ok(Ecosystem::Elixir));
172        assert_eq!("jvm".parse::<Ecosystem>(), Ok(Ecosystem::Java));
173        assert_eq!("scala".parse::<Ecosystem>(), Ok(Ecosystem::Java));
174        assert_eq!("kotlin".parse::<Ecosystem>(), Ok(Ecosystem::Java));
175        assert_eq!("sbt".parse::<Ecosystem>(), Ok(Ecosystem::Java));
176        assert!("unknown".parse::<Ecosystem>().is_err());
177    }
178}