use crate::manifest::{Manifest, ManifestError, Package};
use std::path::Path;
use std::process::Command;
use thiserror::Error;
const REGISTRY_REPO: &str = "fusabi-lang/fusabi-community";
const REGISTRY_FILE: &str = "registry/index.toml";
#[derive(Debug, Error)]
pub enum PublishError {
#[error("Manifest error: {0}")]
Manifest(#[from] ManifestError),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Package validation failed: {0}")]
Validation(String),
#[error("Missing required field: {0}")]
MissingField(String),
}
#[derive(Debug)]
pub struct PublishResult {
pub package_name: String,
pub version: String,
pub registry_entry: String,
pub gh_available: bool,
}
fn validate_package(package: &Package) -> Result<(), PublishError> {
if package.name.is_empty() {
return Err(PublishError::MissingField("name".to_string()));
}
if package.version.is_empty() {
return Err(PublishError::MissingField("version".to_string()));
}
if package.description.is_none() {
return Err(PublishError::Validation(
"Package must have a description for publishing".to_string(),
));
}
if package.repository.is_none() {
return Err(PublishError::Validation(
"Package must have a repository URL for publishing".to_string(),
));
}
Ok(())
}
fn generate_registry_entry(package: &Package) -> String {
let mut entry = format!(
r#"[[packages]]
name = "{}"
version = "{}"
"#,
package.name, package.version
);
if let Some(ref desc) = package.description {
entry.push_str(&format!("description = \"{}\"\n", desc));
}
if let Some(ref repo) = package.repository {
entry.push_str(&format!("repository = \"{}\"\n", repo));
}
if let Some(ref license) = package.license {
entry.push_str(&format!("license = \"{}\"\n", license));
}
if !package.authors.is_empty() {
let authors: Vec<String> = package
.authors
.iter()
.map(|a| format!("\"{}\"", a))
.collect();
entry.push_str(&format!("authors = [{}]\n", authors.join(", ")));
}
entry
}
fn is_gh_available() -> bool {
Command::new("gh")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn publish_package<P: AsRef<Path>>(project_dir: P) -> Result<PublishResult, PublishError> {
let manifest_path = project_dir.as_ref().join("fusabi.toml");
let manifest = Manifest::load(&manifest_path)?;
validate_package(&manifest.package)?;
let registry_entry = generate_registry_entry(&manifest.package);
let gh_available = is_gh_available();
Ok(PublishResult {
package_name: manifest.package.name,
version: manifest.package.version,
registry_entry,
gh_available,
})
}
pub fn print_publish_instructions(result: &PublishResult) {
println!(
"📦 Preparing to publish {} v{}",
result.package_name, result.version
);
println!();
println!("Registry entry to add to {}:", REGISTRY_FILE);
println!("─────────────────────────────────────────");
println!("{}", result.registry_entry);
println!("─────────────────────────────────────────");
println!();
if result.gh_available {
println!("GitHub CLI detected! You can create a PR with:");
println!();
println!(" 1. Fork and clone https://github.com/{}", REGISTRY_REPO);
println!(" 2. Add the above entry to {}", REGISTRY_FILE);
println!(" 3. Create a PR:");
println!(
" gh pr create --title \"Add {} v{}\" --body \"Adds {} package to the registry\"",
result.package_name, result.version, result.package_name
);
} else {
println!("To publish your package:");
println!();
println!(" 1. Fork https://github.com/{}", REGISTRY_REPO);
println!(" 2. Clone your fork locally");
println!(" 3. Add the above entry to {}", REGISTRY_FILE);
println!(" 4. Commit and push your changes");
println!(" 5. Create a pull request to the main repository");
}
println!();
println!("Once your PR is merged, users can install your package with:");
println!(" fpm add {}", result.package_name);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_package_missing_description() {
let package = Package {
name: "test".to_string(),
version: "0.1.0".to_string(),
authors: vec![],
description: None,
license: None,
repository: Some("https://github.com/test/test".to_string()),
};
let result = validate_package(&package);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), PublishError::Validation(_)));
}
#[test]
fn test_validate_package_missing_repository() {
let package = Package {
name: "test".to_string(),
version: "0.1.0".to_string(),
authors: vec![],
description: Some("A test package".to_string()),
license: None,
repository: None,
};
let result = validate_package(&package);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), PublishError::Validation(_)));
}
#[test]
fn test_validate_package_success() {
let package = Package {
name: "test".to_string(),
version: "0.1.0".to_string(),
authors: vec!["Author".to_string()],
description: Some("A test package".to_string()),
license: Some("MIT".to_string()),
repository: Some("https://github.com/test/test".to_string()),
};
let result = validate_package(&package);
assert!(result.is_ok());
}
#[test]
fn test_generate_registry_entry() {
let package = Package {
name: "my-package".to_string(),
version: "1.0.0".to_string(),
authors: vec!["Alice".to_string(), "Bob".to_string()],
description: Some("A cool package".to_string()),
license: Some("MIT".to_string()),
repository: Some("https://github.com/test/my-package".to_string()),
};
let entry = generate_registry_entry(&package);
assert!(entry.contains("name = \"my-package\""));
assert!(entry.contains("version = \"1.0.0\""));
assert!(entry.contains("description = \"A cool package\""));
assert!(entry.contains("repository = \"https://github.com/test/my-package\""));
assert!(entry.contains("license = \"MIT\""));
assert!(entry.contains("authors = [\"Alice\", \"Bob\"]"));
}
#[test]
fn test_generate_registry_entry_minimal() {
let package = Package {
name: "minimal".to_string(),
version: "0.1.0".to_string(),
authors: vec![],
description: Some("Minimal".to_string()),
license: None,
repository: Some("https://github.com/test/minimal".to_string()),
};
let entry = generate_registry_entry(&package);
assert!(entry.contains("name = \"minimal\""));
assert!(entry.contains("version = \"0.1.0\""));
assert!(!entry.contains("license"));
assert!(!entry.contains("authors"));
}
}