use std::path::Path;
use anyhow::{Context, bail};
use crate::DslLanguage;
pub fn detect_language(repo_root: &Path) -> anyhow::Result<DslLanguage> {
let harmont_dir = repo_root.join(".hm");
if !harmont_dir.is_dir() {
bail!("no .hm/ directory found in {}", repo_root.display());
}
let langs = scan_extensions(repo_root)?;
if langs.has_ts {
Ok(DslLanguage::TypeScript)
} else if langs.has_py {
Ok(DslLanguage::Python)
} else {
bail!("no .py or .ts files found in {}", harmont_dir.display())
}
}
pub fn detect_language_python_first(repo_root: &Path) -> anyhow::Result<DslLanguage> {
let harmont_dir = repo_root.join(".hm");
if !harmont_dir.is_dir() {
bail!("no .hm/ directory found in {}", repo_root.display());
}
let langs = scan_extensions(repo_root)?;
if langs.has_py {
Ok(DslLanguage::Python)
} else if langs.has_ts {
Ok(DslLanguage::TypeScript)
} else {
bail!("no .py or .ts files found in {}", harmont_dir.display())
}
}
#[must_use]
pub fn has_pipeline_files(repo_root: &Path) -> bool {
matches!(scan_extensions(repo_root), Ok(langs) if langs.has_py || langs.has_ts)
}
struct DetectedLangs {
has_py: bool,
has_ts: bool,
}
fn scan_extensions(repo_root: &Path) -> anyhow::Result<DetectedLangs> {
let harmont_dir = repo_root.join(".hm");
if !harmont_dir.is_dir() {
return Ok(DetectedLangs {
has_py: false,
has_ts: false,
});
}
let entries = std::fs::read_dir(&harmont_dir)
.with_context(|| format!("failed to read {}", harmont_dir.display()))?;
let mut has_py = false;
let mut has_ts = false;
for entry in entries {
let entry = entry?;
match entry.path().extension().and_then(|e| e.to_str()) {
Some("py") => has_py = true,
Some("ts") => has_ts = true,
_ => {}
}
}
Ok(DetectedLangs { has_py, has_ts })
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn setup(files: &[&str]) -> TempDir {
let tmp = TempDir::new().unwrap();
let harmont = tmp.path().join(".hm");
fs::create_dir(&harmont).unwrap();
for name in files {
fs::write(harmont.join(name), "").unwrap();
}
tmp
}
#[test]
fn python_file_detected() {
let tmp = setup(&["ci.py"]);
let lang = detect_language(tmp.path()).unwrap();
assert_eq!(lang, DslLanguage::Python);
}
#[test]
fn typescript_file_detected() {
let tmp = setup(&["ci.ts"]);
let lang = detect_language(tmp.path()).unwrap();
assert_eq!(lang, DslLanguage::TypeScript);
}
#[test]
fn mixed_languages_prefers_typescript() {
let tmp = setup(&["ci.py", "deploy.ts"]);
let lang = detect_language(tmp.path()).unwrap();
assert_eq!(lang, DslLanguage::TypeScript);
}
#[test]
fn no_harmont_dir_is_error() {
let tmp = TempDir::new().unwrap();
let err = detect_language(tmp.path()).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("no .hm/ directory"), "unexpected error: {msg}");
}
#[test]
fn empty_harmont_dir_is_error() {
let tmp = TempDir::new().unwrap();
fs::create_dir(tmp.path().join(".hm")).unwrap();
let err = detect_language(tmp.path()).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("no .py or .ts files"),
"unexpected error: {msg}"
);
}
#[test]
fn python_first_prefers_python_when_mixed() {
let tmp = setup(&["ci.py", "deploy.ts"]);
assert_eq!(
detect_language_python_first(tmp.path()).unwrap(),
DslLanguage::Python
);
}
#[test]
fn python_first_falls_back_to_typescript_when_only_ts() {
let tmp = setup(&["ci.ts"]);
assert_eq!(
detect_language_python_first(tmp.path()).unwrap(),
DslLanguage::TypeScript
);
}
#[test]
fn python_first_no_harmont_dir_is_error() {
let tmp = TempDir::new().unwrap();
let err = detect_language_python_first(tmp.path()).unwrap_err();
assert!(
err.to_string().contains("no .hm/ directory"),
"unexpected error: {err}"
);
}
#[test]
fn has_pipeline_files_true_for_py_and_ts() {
assert!(has_pipeline_files(setup(&["ci.py"]).path()));
assert!(has_pipeline_files(setup(&["ci.ts"]).path()));
assert!(has_pipeline_files(setup(&["ci.py", "deploy.ts"]).path()));
}
#[test]
fn has_pipeline_files_false_for_missing_or_empty_harmont() {
assert!(!has_pipeline_files(TempDir::new().unwrap().path()));
assert!(!has_pipeline_files(setup(&["README.md"]).path()));
}
}