1use std::{fmt, path::Path};
2
3use serde::{Deserialize, Serialize};
4
5#[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 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
54fn detect_from_command(command: &str) -> Option<Ecosystem> {
56 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" | "npm" | "npx" | "yarn" | "pnpm" | "bun" => Some(Ecosystem::Node),
65 "cargo" | "rustc" | "rustup" => Some(Ecosystem::Rust),
67 "python" | "python3" | "pip" | "pip3" | "uv" | "poetry" | "pdm" | "rye" => {
69 Some(Ecosystem::Python)
70 }
71 "mix" | "elixir" | "iex" => Some(Ecosystem::Elixir),
73 "java" | "javac" | "mvn" | "mvnw" | "gradle" | "gradlew" | "sbt" | "scala" | "scalac"
75 | "kotlinc" => Some(Ecosystem::Java),
76 _ => None,
77 }
78}
79
80fn detect_from_files(pwd: &Path) -> Option<Ecosystem> {
82 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
105pub 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}