use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageManifest {
pub name: String,
pub version: String,
pub format_version: String,
pub created_at: DateTime<Utc>,
pub author: Option<String>,
pub description: Option<String>,
pub license: Option<String>,
pub modules: Vec<ModuleInfo>,
pub resources: Vec<ResourceInfo>,
pub dependencies: HashMap<String, String>,
pub metadata: HashMap<String, String>,
pub signature: Option<PackageSignature>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModuleInfo {
pub name: String,
pub class_name: String,
pub version: String,
pub dependencies: Vec<String>,
pub has_source: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceInfo {
pub name: String,
pub resource_type: String,
pub size: u64,
pub sha256: String,
pub compression: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageSignature {
pub algorithm: String,
pub key_id: String,
pub signature: String,
pub signed_at: DateTime<Utc>,
}
impl PackageManifest {
pub fn new(name: String, version: String) -> Self {
Self {
name,
version,
format_version: crate::PACKAGE_FORMAT_VERSION.to_string(),
created_at: Utc::now(),
author: None,
description: None,
license: None,
modules: Vec::new(),
resources: Vec::new(),
dependencies: HashMap::new(),
metadata: HashMap::new(),
signature: None,
}
}
pub fn validate(&self) -> Result<(), String> {
if self.name.is_empty() {
return Err("Package name cannot be empty".to_string());
}
if self.version.is_empty() {
return Err("Package version cannot be empty".to_string());
}
if semver::Version::parse(&self.version).is_err() {
return Err(format!("Invalid version format: {}", self.version));
}
let current_version = semver::Version::parse(crate::PACKAGE_FORMAT_VERSION)
.expect("PACKAGE_FORMAT_VERSION constant should be valid semver");
let manifest_version = semver::Version::parse(&self.format_version)
.map_err(|_| "Invalid format version in manifest")?;
if manifest_version.major != current_version.major {
return Err(format!(
"Incompatible package format version: {} (expected {}.x.x)",
self.format_version, current_version.major
));
}
for module in &self.modules {
if module.name.is_empty() {
return Err("Module name cannot be empty".to_string());
}
}
Ok(())
}
pub fn with_author(mut self, author: String) -> Self {
self.author = Some(author);
self
}
pub fn with_description(mut self, description: String) -> Self {
self.description = Some(description);
self
}
pub fn with_license(mut self, license: String) -> Self {
self.license = Some(license);
self
}
pub fn total_size(&self) -> u64 {
self.resources.iter().map(|r| r.size).sum()
}
pub fn get_module(&self, name: &str) -> Option<&ModuleInfo> {
self.modules.iter().find(|m| m.name == name)
}
pub fn get_resource(&self, name: &str) -> Option<&ResourceInfo> {
self.resources.iter().find(|r| r.name == name)
}
}
pub struct ManifestBuilder {
manifest: PackageManifest,
}
impl ManifestBuilder {
pub fn new(name: String, version: String) -> Self {
Self {
manifest: PackageManifest::new(name, version),
}
}
pub fn author(mut self, author: String) -> Self {
self.manifest.author = Some(author);
self
}
pub fn description(mut self, description: String) -> Self {
self.manifest.description = Some(description);
self
}
pub fn license(mut self, license: String) -> Self {
self.manifest.license = Some(license);
self
}
pub fn add_module(mut self, module: ModuleInfo) -> Self {
self.manifest.modules.push(module);
self
}
pub fn add_resource(mut self, resource: ResourceInfo) -> Self {
self.manifest.resources.push(resource);
self
}
pub fn add_dependency(mut self, name: String, version: String) -> Self {
self.manifest.dependencies.insert(name, version);
self
}
pub fn add_metadata(mut self, key: String, value: String) -> Self {
self.manifest.metadata.insert(key, value);
self
}
pub fn build(self) -> PackageManifest {
self.manifest
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_manifest_creation() {
let manifest = PackageManifest::new("test_package".to_string(), "1.0.0".to_string());
assert_eq!(manifest.name, "test_package");
assert_eq!(manifest.version, "1.0.0");
assert_eq!(manifest.format_version, crate::PACKAGE_FORMAT_VERSION);
}
#[test]
fn test_manifest_validation() {
let mut manifest = PackageManifest::new("test".to_string(), "1.0.0".to_string());
assert!(manifest.validate().is_ok());
manifest.version = "invalid".to_string();
assert!(manifest.validate().is_err());
manifest.version = "1.0.0".to_string();
manifest.name = String::new();
assert!(manifest.validate().is_err());
}
#[test]
fn test_manifest_builder() {
let manifest = ManifestBuilder::new("test".to_string(), "1.0.0".to_string())
.author("Test Author".to_string())
.description("Test package".to_string())
.license("MIT".to_string())
.add_dependency("torsh-core".to_string(), "0.1.0-alpha.2".to_string())
.build();
assert_eq!(manifest.author.as_deref(), Some("Test Author"));
assert_eq!(manifest.description.as_deref(), Some("Test package"));
assert_eq!(manifest.license.as_deref(), Some("MIT"));
assert_eq!(
manifest.dependencies.get("torsh-core"),
Some(&"0.1.0-alpha.2".to_string())
);
}
}