1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct PackageManifest {
10 pub name: String,
12
13 pub version: String,
15
16 pub format_version: String,
18
19 pub created_at: DateTime<Utc>,
21
22 pub author: Option<String>,
24
25 pub description: Option<String>,
27
28 pub license: Option<String>,
30
31 pub modules: Vec<ModuleInfo>,
33
34 pub resources: Vec<ResourceInfo>,
36
37 pub dependencies: HashMap<String, String>,
39
40 pub metadata: HashMap<String, String>,
42
43 pub signature: Option<PackageSignature>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct ModuleInfo {
50 pub name: String,
52
53 pub class_name: String,
55
56 pub version: String,
58
59 pub dependencies: Vec<String>,
61
62 pub has_source: bool,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct ResourceInfo {
69 pub name: String,
71
72 pub resource_type: String,
74
75 pub size: u64,
77
78 pub sha256: String,
80
81 pub compression: Option<String>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct PackageSignature {
88 pub algorithm: String,
90
91 pub key_id: String,
93
94 pub signature: String,
96
97 pub signed_at: DateTime<Utc>,
99}
100
101impl PackageManifest {
102 pub fn new(name: String, version: String) -> Self {
104 Self {
105 name,
106 version,
107 format_version: crate::PACKAGE_FORMAT_VERSION.to_string(),
108 created_at: Utc::now(),
109 author: None,
110 description: None,
111 license: None,
112 modules: Vec::new(),
113 resources: Vec::new(),
114 dependencies: HashMap::new(),
115 metadata: HashMap::new(),
116 signature: None,
117 }
118 }
119
120 pub fn validate(&self) -> Result<(), String> {
122 if self.name.is_empty() {
124 return Err("Package name cannot be empty".to_string());
125 }
126
127 if self.version.is_empty() {
128 return Err("Package version cannot be empty".to_string());
129 }
130
131 if semver::Version::parse(&self.version).is_err() {
133 return Err(format!("Invalid version format: {}", self.version));
134 }
135
136 let current_version = semver::Version::parse(crate::PACKAGE_FORMAT_VERSION)
138 .expect("PACKAGE_FORMAT_VERSION constant should be valid semver");
139 let manifest_version = semver::Version::parse(&self.format_version)
140 .map_err(|_| "Invalid format version in manifest")?;
141
142 if manifest_version.major != current_version.major {
143 return Err(format!(
144 "Incompatible package format version: {} (expected {}.x.x)",
145 self.format_version, current_version.major
146 ));
147 }
148
149 for module in &self.modules {
151 if module.name.is_empty() {
152 return Err("Module name cannot be empty".to_string());
153 }
154 }
155
156 Ok(())
157 }
158
159 pub fn with_author(mut self, author: String) -> Self {
161 self.author = Some(author);
162 self
163 }
164
165 pub fn with_description(mut self, description: String) -> Self {
167 self.description = Some(description);
168 self
169 }
170
171 pub fn with_license(mut self, license: String) -> Self {
173 self.license = Some(license);
174 self
175 }
176
177 pub fn total_size(&self) -> u64 {
179 self.resources.iter().map(|r| r.size).sum()
180 }
181
182 pub fn get_module(&self, name: &str) -> Option<&ModuleInfo> {
184 self.modules.iter().find(|m| m.name == name)
185 }
186
187 pub fn get_resource(&self, name: &str) -> Option<&ResourceInfo> {
189 self.resources.iter().find(|r| r.name == name)
190 }
191}
192
193pub struct ManifestBuilder {
195 manifest: PackageManifest,
196}
197
198impl ManifestBuilder {
199 pub fn new(name: String, version: String) -> Self {
201 Self {
202 manifest: PackageManifest::new(name, version),
203 }
204 }
205
206 pub fn author(mut self, author: String) -> Self {
208 self.manifest.author = Some(author);
209 self
210 }
211
212 pub fn description(mut self, description: String) -> Self {
214 self.manifest.description = Some(description);
215 self
216 }
217
218 pub fn license(mut self, license: String) -> Self {
220 self.manifest.license = Some(license);
221 self
222 }
223
224 pub fn add_module(mut self, module: ModuleInfo) -> Self {
226 self.manifest.modules.push(module);
227 self
228 }
229
230 pub fn add_resource(mut self, resource: ResourceInfo) -> Self {
232 self.manifest.resources.push(resource);
233 self
234 }
235
236 pub fn add_dependency(mut self, name: String, version: String) -> Self {
238 self.manifest.dependencies.insert(name, version);
239 self
240 }
241
242 pub fn add_metadata(mut self, key: String, value: String) -> Self {
244 self.manifest.metadata.insert(key, value);
245 self
246 }
247
248 pub fn build(self) -> PackageManifest {
250 self.manifest
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn test_manifest_creation() {
260 let manifest = PackageManifest::new("test_package".to_string(), "1.0.0".to_string());
261
262 assert_eq!(manifest.name, "test_package");
263 assert_eq!(manifest.version, "1.0.0");
264 assert_eq!(manifest.format_version, crate::PACKAGE_FORMAT_VERSION);
265 }
266
267 #[test]
268 fn test_manifest_validation() {
269 let mut manifest = PackageManifest::new("test".to_string(), "1.0.0".to_string());
270 assert!(manifest.validate().is_ok());
271
272 manifest.version = "invalid".to_string();
274 assert!(manifest.validate().is_err());
275
276 manifest.version = "1.0.0".to_string();
278 manifest.name = String::new();
279 assert!(manifest.validate().is_err());
280 }
281
282 #[test]
283 fn test_manifest_builder() {
284 let manifest = ManifestBuilder::new("test".to_string(), "1.0.0".to_string())
285 .author("Test Author".to_string())
286 .description("Test package".to_string())
287 .license("MIT".to_string())
288 .add_dependency("torsh-core".to_string(), "0.1.0-alpha.2".to_string())
289 .build();
290
291 assert_eq!(manifest.author.as_deref(), Some("Test Author"));
292 assert_eq!(manifest.description.as_deref(), Some("Test package"));
293 assert_eq!(manifest.license.as_deref(), Some("MIT"));
294 assert_eq!(
295 manifest.dependencies.get("torsh-core"),
296 Some(&"0.1.0-alpha.2".to_string())
297 );
298 }
299}