use crate::utils::package_installer::PackageInstaller;
use crate::utils::package_manager::PackageManagerImpl;
use crate::utils::profile_manifest::{Package, PackageManager};
use crate::utils::ProfileManifest;
use anyhow::Result;
use std::path::Path;
use std::process::Command;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PackageCheckStatus {
Installed,
NotInstalled,
Error(String),
Unknown,
}
#[derive(Debug)]
pub struct PackageCheckResult {
pub status: PackageCheckStatus,
pub used_fallback: bool,
}
#[derive(Debug)]
pub struct PackageValidation {
pub is_valid: bool,
pub error_message: Option<String>,
}
#[derive(Debug)]
pub struct PackageCreationParams<'a> {
pub name: &'a str,
pub description: &'a str,
pub manager: PackageManager,
pub is_custom: bool,
pub package_name: &'a str,
pub binary_name: &'a str,
pub install_command: &'a str,
pub existence_check: &'a str,
pub manager_check: &'a str,
}
pub struct PackageService;
impl PackageService {
#[must_use]
pub fn get_available_managers() -> Vec<PackageManager> {
PackageManagerImpl::get_available_managers()
}
#[must_use]
pub fn is_manager_installed(manager: &PackageManager) -> bool {
PackageManagerImpl::is_manager_installed(manager)
}
pub fn check_package(package: &Package) -> PackageCheckResult {
match PackageInstaller::check_exists(package) {
Ok((true, check_cmd, _out)) => {
let used_fallback = check_cmd
.as_ref()
.is_some_and(|cmd| !cmd.starts_with("which "));
debug!(
"Package {} found (used_fallback: {})",
package.name, used_fallback
);
PackageCheckResult {
status: PackageCheckStatus::Installed,
used_fallback,
}
}
Ok((false, _, _)) => {
if PackageManagerImpl::is_manager_installed(&package.manager) {
debug!(
"Package {} not found (manager {:?} is available)",
package.name, package.manager
);
PackageCheckResult {
status: PackageCheckStatus::NotInstalled,
used_fallback: false,
}
} else {
warn!(
"Package {} not found and manager {:?} is not installed",
package.name, package.manager
);
PackageCheckResult {
status: PackageCheckStatus::Error(format!(
"Package not found and package manager '{:?}' is not installed",
package.manager
)),
used_fallback: false,
}
}
}
Err(e) => {
warn!("Error checking package {}: {}", package.name, e);
PackageCheckResult {
status: PackageCheckStatus::Error(format!("Error checking package: {e}")),
used_fallback: false,
}
}
}
}
pub fn validate_package(
name: &str,
binary_name: &str,
is_custom: bool,
package_name: &str,
install_command: &str,
manager: Option<&PackageManager>,
) -> PackageValidation {
if name.trim().is_empty() {
warn!("Package validation failed: name is empty");
return PackageValidation {
is_valid: false,
error_message: Some("Name is required".to_string()),
};
}
if binary_name.trim().is_empty() {
warn!("Package validation failed: binary_name is empty");
return PackageValidation {
is_valid: false,
error_message: Some("Binary name is required".to_string()),
};
}
if manager.is_none() {
return PackageValidation {
is_valid: false,
error_message: Some("Package manager not selected".to_string()),
};
}
if is_custom {
if install_command.trim().is_empty() {
warn!("Package validation failed: install_command is empty for custom package");
return PackageValidation {
is_valid: false,
error_message: Some(
"Install command is required for custom packages".to_string(),
),
};
}
} else {
if package_name.trim().is_empty() {
warn!("Package validation failed: package_name is empty for managed package");
return PackageValidation {
is_valid: false,
error_message: Some(
"Package name is required for managed packages".to_string(),
),
};
}
}
PackageValidation {
is_valid: true,
error_message: None,
}
}
#[must_use]
pub fn create_package(params: PackageCreationParams) -> Package {
Package {
name: params.name.trim().to_string(),
description: if params.description.trim().is_empty() {
None
} else {
Some(params.description.trim().to_string())
},
manager: params.manager,
package_name: if params.is_custom {
None
} else {
Some(params.package_name.trim().to_string())
},
binary_name: params.binary_name.trim().to_string(),
install_command: if params.is_custom {
Some(params.install_command.trim().to_string())
} else {
None
},
existence_check: if params.is_custom && !params.existence_check.trim().is_empty() {
Some(params.existence_check.trim().to_string())
} else {
None
},
manager_check: if params.manager_check.trim().is_empty() {
None
} else {
Some(params.manager_check.trim().to_string())
},
}
}
pub fn add_package(
repo_path: &Path,
profile_name: &str,
package: Package,
) -> Result<Vec<Package>> {
info!(
"Adding new package: {} (profile: {})",
package.name, profile_name
);
let mut manifest = ProfileManifest::load_or_backfill(repo_path)?;
if let Some(profile) = manifest
.profiles
.iter_mut()
.find(|p| p.name == profile_name)
{
profile.packages.push(package);
let packages = profile.packages.clone();
manifest.save(repo_path)?;
Ok(packages)
} else {
Err(anyhow::anyhow!("Profile '{profile_name}' not found"))
}
}
pub fn update_package(
repo_path: &Path,
profile_name: &str,
index: usize,
package: Package,
) -> Result<Vec<Package>> {
let mut manifest = ProfileManifest::load_or_backfill(repo_path)?;
if let Some(profile) = manifest
.profiles
.iter_mut()
.find(|p| p.name == profile_name)
{
if index < profile.packages.len() {
let old_name = profile.packages[index].name.clone();
info!(
"Updating package: {} -> {} (profile: {})",
old_name, package.name, profile_name
);
profile.packages[index] = package;
let packages = profile.packages.clone();
manifest.save(repo_path)?;
Ok(packages)
} else {
Err(anyhow::anyhow!(
"Package index {} out of bounds ({} packages)",
index,
profile.packages.len()
))
}
} else {
Err(anyhow::anyhow!("Profile '{profile_name}' not found"))
}
}
pub fn delete_package(
repo_path: &Path,
profile_name: &str,
index: usize,
) -> Result<Vec<Package>> {
let mut manifest = ProfileManifest::load_or_backfill(repo_path)?;
if let Some(profile) = manifest
.profiles
.iter_mut()
.find(|p| p.name == profile_name)
{
if index < profile.packages.len() {
let package_name = profile.packages[index].name.clone();
info!(
"Deleting package: {} (index: {}, profile: {})",
package_name, index, profile_name
);
profile.packages.remove(index);
let packages = profile.packages.clone();
manifest.save(repo_path)?;
Ok(packages)
} else {
Err(anyhow::anyhow!(
"Package index {} out of bounds ({} packages)",
index,
profile.packages.len()
))
}
} else {
Err(anyhow::anyhow!("Profile '{profile_name}' not found"))
}
}
pub fn get_packages(repo_path: &Path, profile_name: &str) -> Result<Vec<Package>> {
let manifest = ProfileManifest::load_or_backfill(repo_path)?;
Ok(manifest
.profiles
.into_iter()
.find(|p| p.name == profile_name)
.map(|p| p.packages)
.unwrap_or_default())
}
#[must_use]
pub fn get_install_command(package: &Package) -> Command {
PackageManagerImpl::get_install_command_builder(package)
}
pub fn start_install(
package: &Package,
) -> Result<crate::utils::package_installer::InstallationHandle> {
PackageInstaller::start_install(package)
}
}
#[cfg(test)]
mod tests {
use super::PackageCreationParams;
use super::*;
#[test]
fn test_validate_package_empty_name() {
let result = PackageService::validate_package(
"",
"binary",
false,
"package",
"",
Some(&PackageManager::Brew),
);
assert!(!result.is_valid);
assert!(result.error_message.unwrap().contains("Name"));
}
#[test]
fn test_validate_package_empty_binary() {
let result = PackageService::validate_package(
"name",
"",
false,
"package",
"",
Some(&PackageManager::Brew),
);
assert!(!result.is_valid);
assert!(result.error_message.unwrap().contains("Binary"));
}
#[test]
fn test_validate_custom_package_no_install_command() {
let result = PackageService::validate_package(
"name",
"binary",
true,
"",
"",
Some(&PackageManager::Custom),
);
assert!(!result.is_valid);
assert!(result.error_message.unwrap().contains("Install command"));
}
#[test]
fn test_validate_managed_package_no_package_name() {
let result = PackageService::validate_package(
"name",
"binary",
false,
"",
"",
Some(&PackageManager::Brew),
);
assert!(!result.is_valid);
assert!(result.error_message.unwrap().contains("Package name"));
}
#[test]
fn test_validate_valid_managed_package() {
let result = PackageService::validate_package(
"Git",
"git",
false,
"git",
"",
Some(&PackageManager::Brew),
);
assert!(result.is_valid);
assert!(result.error_message.is_none());
}
#[test]
fn test_create_package() {
let package = PackageService::create_package(PackageCreationParams {
name: "Git",
description: "Version control",
manager: PackageManager::Brew,
is_custom: false,
package_name: "git",
binary_name: "git",
install_command: "",
existence_check: "",
manager_check: "",
});
assert_eq!(package.name, "Git");
assert_eq!(package.description, Some("Version control".to_string()));
assert_eq!(package.binary_name, "git");
assert_eq!(package.package_name, Some("git".to_string()));
assert!(package.install_command.is_none());
}
}