use std::path::{Path, PathBuf};
const COMPOSE_FILE_CANDIDATES: [&str; 4] = [
"compose.yaml",
"compose.yml",
"docker-compose.yaml",
"docker-compose.yml",
];
pub(crate) fn resolve_compose_files(explicit: &[PathBuf]) -> Vec<PathBuf> {
if !explicit.is_empty() {
return explicit.to_vec();
}
if let Ok(env) = std::env::var("COMPOSE_FILE") {
if !env.is_empty() {
let sep = if cfg!(windows) { ';' } else { ':' };
return env.split(sep).map(PathBuf::from).collect();
}
}
for candidate in COMPOSE_FILE_CANDIDATES {
if Path::new(candidate).is_file() {
return vec![PathBuf::from(candidate)];
}
}
vec![PathBuf::from("docker-compose.yml")]
}
pub(crate) fn resolve_base_dir(project_directory: Option<&Path>, file: &Path) -> PathBuf {
if let Some(dir) = project_directory {
return dir.to_path_buf();
}
match file.parent() {
Some(parent) if !parent.as_os_str().is_empty() => parent.to_path_buf(),
_ => std::env::current_dir().unwrap_or_default(),
}
}
pub(crate) fn resolve_project_name(
explicit: Option<String>,
compose_name: Option<&str>,
base_dir: &Path,
) -> String {
if let Some(name) = explicit {
return name;
}
if let Some(name) = compose_name {
return name.to_string();
}
let probe = if base_dir.as_os_str().is_empty() {
Path::new(".")
} else {
base_dir
};
let basename = probe
.canonicalize()
.unwrap_or_else(|_| probe.to_path_buf())
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
sanitize_project_name(&basename)
}
pub(crate) fn sanitize_project_name(raw: &str) -> String {
let kept: String = raw
.to_lowercase()
.chars()
.filter(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || *c == '_' || *c == '-')
.collect();
let trimmed = kept.trim_start_matches(['_', '-']);
if trimmed.is_empty() {
"podup".to_string()
} else {
trimmed.to_string()
}
}
#[cfg(test)]
mod tests {
use super::{
resolve_base_dir, resolve_compose_files, resolve_project_name, sanitize_project_name,
};
use std::path::{Path, PathBuf};
#[test]
fn explicit_compose_files_win() {
let p = resolve_compose_files(&[PathBuf::from("custom.yml")]);
assert_eq!(p, vec![PathBuf::from("custom.yml")]);
}
#[test]
fn multiple_explicit_compose_files_preserved() {
let p = resolve_compose_files(&[PathBuf::from("a.yml"), PathBuf::from("b.yml")]);
assert_eq!(p, vec![PathBuf::from("a.yml"), PathBuf::from("b.yml")]);
}
#[test]
fn missing_compose_file_falls_back_to_default_name() {
let dir = std::env::temp_dir().join(format!("podup-cf-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let p = temp_env::with_var_unset("COMPOSE_FILE", || resolve_compose_files(&[]));
std::env::set_current_dir(prev).unwrap();
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(p, vec![PathBuf::from("docker-compose.yml")]);
}
#[test]
fn project_directory_override_wins() {
let base = resolve_base_dir(
Some(Path::new("/srv/app")),
Path::new("/etc/compose/docker-compose.yml"),
);
assert_eq!(base, PathBuf::from("/srv/app"));
}
#[test]
fn defaults_to_compose_file_parent() {
let base = resolve_base_dir(None, Path::new("/etc/compose/docker-compose.yml"));
assert_eq!(base, PathBuf::from("/etc/compose"));
}
#[test]
fn bare_filename_resolves_to_current_dir() {
let base = resolve_base_dir(None, Path::new("docker-compose.yml"));
assert_eq!(base, std::env::current_dir().unwrap());
assert!(base.is_absolute());
}
#[test]
fn explicit_project_name_wins() {
let name = resolve_project_name(
Some("explicit".to_string()),
Some("from-compose"),
Path::new("/srv/myapp"),
);
assert_eq!(name, "explicit");
}
#[test]
fn compose_name_used_when_no_explicit() {
let name = resolve_project_name(None, Some("from-compose"), Path::new("/srv/myapp"));
assert_eq!(name, "from-compose");
}
#[cfg(unix)]
#[test]
fn falls_back_to_directory_basename() {
let dir = std::env::temp_dir().join(format!("podup-pn-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let name = resolve_project_name(None, None, &dir);
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(
name,
dir.file_name().unwrap().to_string_lossy().to_lowercase()
);
}
#[test]
fn sanitize_lowercases_and_drops_invalid_chars() {
assert_eq!(sanitize_project_name("My App!"), "myapp");
}
#[test]
fn sanitize_keeps_underscore_and_hyphen() {
assert_eq!(sanitize_project_name("web_service-1"), "web_service-1");
}
#[test]
fn sanitize_strips_leading_separators() {
assert_eq!(sanitize_project_name("__leading"), "leading");
assert_eq!(sanitize_project_name("--dash"), "dash");
}
#[test]
fn sanitize_empty_result_falls_back_to_podup() {
assert_eq!(sanitize_project_name("!!!"), "podup");
assert_eq!(sanitize_project_name(""), "podup");
}
}