ggen_core/lockfile_unified/
entry.rs1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9use std::path::PathBuf;
10
11use super::traits::LockEntry;
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
20pub struct UnifiedLockEntry {
21 #[serde(skip)]
23 id: String,
24
25 pub version: String,
27
28 pub integrity: String,
30
31 pub source: LockSource,
33
34 pub locked_at: DateTime<Utc>,
36
37 #[serde(default, skip_serializing_if = "Vec::is_empty")]
39 pub dependencies: Vec<String>,
40
41 #[serde(default, skip_serializing_if = "ExtendedMetadata::is_empty")]
43 pub metadata: ExtendedMetadata,
44
45 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub pqc: Option<PqcSignature>,
48}
49
50impl UnifiedLockEntry {
51 pub fn new(
53 id: impl Into<String>, version: impl Into<String>, integrity: impl Into<String>,
54 source: LockSource,
55 ) -> Self {
56 Self {
57 id: id.into(),
58 version: version.into(),
59 integrity: integrity.into(),
60 source,
61 locked_at: Utc::now(),
62 dependencies: Vec::new(),
63 metadata: ExtendedMetadata::default(),
64 pqc: None,
65 }
66 }
67
68 pub fn with_id(mut self, id: impl Into<String>) -> Self {
70 self.id = id.into();
71 self
72 }
73
74 pub fn with_dependencies(mut self, deps: Vec<String>) -> Self {
76 self.dependencies = deps;
77 self
78 }
79
80 pub fn with_pqc(mut self, pqc: PqcSignature) -> Self {
82 self.pqc = Some(pqc);
83 self
84 }
85
86 pub fn with_metadata(mut self, metadata: ExtendedMetadata) -> Self {
88 self.metadata = metadata;
89 self
90 }
91}
92
93impl LockEntry for UnifiedLockEntry {
94 fn id(&self) -> &str {
95 &self.id
96 }
97
98 fn version(&self) -> &str {
99 &self.version
100 }
101
102 fn integrity(&self) -> Option<&str> {
103 Some(&self.integrity)
104 }
105
106 fn dependencies(&self) -> &[String] {
107 &self.dependencies
108 }
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
113#[serde(tag = "type")]
114pub enum LockSource {
115 Registry {
117 url: String,
119 #[serde(skip_serializing_if = "Option::is_none")]
121 resolved: Option<String>,
122 },
123
124 Git {
126 url: String,
128 #[serde(skip_serializing_if = "Option::is_none")]
130 branch: Option<String>,
131 #[serde(skip_serializing_if = "Option::is_none")]
133 tag: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none")]
136 commit: Option<String>,
137 },
138
139 GitHub {
141 org: String,
143 repo: String,
145 branch: String,
147 },
148
149 Local {
151 path: PathBuf,
153 },
154}
155
156impl std::fmt::Display for LockSource {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 match self {
159 LockSource::Registry { url, .. } => write!(f, "registry+{}", url),
160 LockSource::Git { url, branch, .. } => {
161 if let Some(b) = branch {
162 write!(f, "git+{}#{}", url, b)
163 } else {
164 write!(f, "git+{}", url)
165 }
166 }
167 LockSource::GitHub { org, repo, branch } => {
168 write!(f, "github:{}/{}#{}", org, repo, branch)
169 }
170 LockSource::Local { path } => write!(f, "path:{}", path.display()),
171 }
172 }
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
177pub struct PqcSignature {
178 pub algorithm: String,
180 pub signature: String,
182 pub pubkey: String,
184}
185
186impl PqcSignature {
187 pub fn new(
189 algorithm: impl Into<String>, signature: impl Into<String>, pubkey: impl Into<String>,
190 ) -> Self {
191 Self {
192 algorithm: algorithm.into(),
193 signature: signature.into(),
194 pubkey: pubkey.into(),
195 }
196 }
197
198 pub fn ml_dsa_65(signature: impl Into<String>, pubkey: impl Into<String>) -> Self {
200 Self::new("ML-DSA-65", signature, pubkey)
201 }
202}
203
204#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
206pub struct ExtendedMetadata {
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub namespace: Option<String>,
210
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub classes_count: Option<usize>,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub properties_count: Option<usize>,
218
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub composition_strategy: Option<String>,
222
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub installed_at: Option<DateTime<Utc>>,
226
227 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
229 pub custom: BTreeMap<String, String>,
230}
231
232impl ExtendedMetadata {
233 pub fn is_empty(&self) -> bool {
235 self.namespace.is_none()
236 && self.classes_count.is_none()
237 && self.properties_count.is_none()
238 && self.composition_strategy.is_none()
239 && self.installed_at.is_none()
240 && self.custom.is_empty()
241 }
242
243 pub fn ontology(namespace: impl Into<String>, classes: usize, properties: usize) -> Self {
245 Self {
246 namespace: Some(namespace.into()),
247 classes_count: Some(classes),
248 properties_count: Some(properties),
249 ..Default::default()
250 }
251 }
252}
253
254impl From<crate::lockfile::LockEntry> for UnifiedLockEntry {
260 fn from(entry: crate::lockfile::LockEntry) -> Self {
261 Self {
262 id: entry.id.clone(),
263 version: entry.version,
264 integrity: entry.sha256,
265 source: LockSource::Git {
266 url: entry.source,
267 branch: None,
268 tag: None,
269 commit: None,
270 },
271 locked_at: Utc::now(),
272 dependencies: entry.dependencies.unwrap_or_default(),
273 metadata: ExtendedMetadata::default(),
274 pqc: entry.pqc_signature.map(|sig| PqcSignature {
275 algorithm: "Dilithium3".to_string(),
276 signature: sig,
277 pubkey: entry.pqc_pubkey.unwrap_or_default(),
278 }),
279 }
280 }
281}
282
283impl From<crate::packs::lockfile::LockedPack> for UnifiedLockEntry {
285 fn from(pack: crate::packs::lockfile::LockedPack) -> Self {
286 Self {
287 id: String::new(), version: pack.version,
289 integrity: pack.integrity.unwrap_or_default(),
290 source: match pack.source {
291 crate::packs::lockfile::PackSource::Registry { url } => LockSource::Registry {
292 url,
293 resolved: None,
294 },
295 crate::packs::lockfile::PackSource::GitHub { org, repo, branch } => {
296 LockSource::GitHub { org, repo, branch }
297 }
298 crate::packs::lockfile::PackSource::Local { path } => LockSource::Local { path },
299 },
300 locked_at: pack.installed_at,
301 dependencies: pack.dependencies,
302 metadata: ExtendedMetadata {
303 installed_at: Some(pack.installed_at),
304 ..Default::default()
305 },
306 pqc: None,
307 }
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn test_unified_entry_creation() {
317 let entry = UnifiedLockEntry::new(
318 "io.ggen.test",
319 "1.0.0",
320 "abc123def456",
321 LockSource::Registry {
322 url: "https://registry.ggen.io".into(),
323 resolved: None,
324 },
325 );
326
327 assert_eq!(entry.version, "1.0.0");
328 assert_eq!(entry.integrity, "abc123def456");
329 }
330
331 #[test]
332 fn test_lock_source_display() {
333 let registry = LockSource::Registry {
334 url: "https://registry.ggen.io".into(),
335 resolved: None,
336 };
337 assert_eq!(registry.to_string(), "registry+https://registry.ggen.io");
338
339 let github = LockSource::GitHub {
340 org: "seanchatmangpt".into(),
341 repo: "ggen".into(),
342 branch: "main".into(),
343 };
344 assert_eq!(github.to_string(), "github:seanchatmangpt/ggen#main");
345 }
346
347 #[test]
348 fn test_extended_metadata_is_empty() {
349 let empty = ExtendedMetadata::default();
350 assert!(empty.is_empty());
351
352 let with_namespace = ExtendedMetadata {
353 namespace: Some("https://schema.org/".into()),
354 ..Default::default()
355 };
356 assert!(!with_namespace.is_empty());
357 }
358
359 #[test]
360 fn test_pqc_signature_creation() {
361 let sig = PqcSignature::ml_dsa_65("base64sig", "base64pubkey");
362 assert_eq!(sig.algorithm, "ML-DSA-65");
363 }
364}