use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::PathBuf;
use super::traits::LockEntry;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct UnifiedLockEntry {
#[serde(skip)]
id: String,
pub version: String,
pub integrity: String,
pub source: LockSource,
pub locked_at: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dependencies: Vec<String>,
#[serde(default, skip_serializing_if = "ExtendedMetadata::is_empty")]
pub metadata: ExtendedMetadata,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pqc: Option<PqcSignature>,
}
impl UnifiedLockEntry {
pub fn new(
id: impl Into<String>, version: impl Into<String>, integrity: impl Into<String>,
source: LockSource,
) -> Self {
Self {
id: id.into(),
version: version.into(),
integrity: integrity.into(),
source,
locked_at: Utc::now(),
dependencies: Vec::new(),
metadata: ExtendedMetadata::default(),
pqc: None,
}
}
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = id.into();
self
}
pub fn with_dependencies(mut self, deps: Vec<String>) -> Self {
self.dependencies = deps;
self
}
pub fn with_pqc(mut self, pqc: PqcSignature) -> Self {
self.pqc = Some(pqc);
self
}
pub fn with_metadata(mut self, metadata: ExtendedMetadata) -> Self {
self.metadata = metadata;
self
}
}
impl LockEntry for UnifiedLockEntry {
fn id(&self) -> &str {
&self.id
}
fn version(&self) -> &str {
&self.version
}
fn integrity(&self) -> Option<&str> {
Some(&self.integrity)
}
fn dependencies(&self) -> &[String] {
&self.dependencies
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum LockSource {
Registry {
url: String,
#[serde(skip_serializing_if = "Option::is_none")]
resolved: Option<String>,
},
Git {
url: String,
#[serde(skip_serializing_if = "Option::is_none")]
branch: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
tag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
commit: Option<String>,
},
GitHub {
org: String,
repo: String,
branch: String,
},
Local {
path: PathBuf,
},
}
impl std::fmt::Display for LockSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LockSource::Registry { url, .. } => write!(f, "registry+{}", url),
LockSource::Git { url, branch, .. } => {
if let Some(b) = branch {
write!(f, "git+{}#{}", url, b)
} else {
write!(f, "git+{}", url)
}
}
LockSource::GitHub { org, repo, branch } => {
write!(f, "github:{}/{}#{}", org, repo, branch)
}
LockSource::Local { path } => write!(f, "path:{}", path.display()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PqcSignature {
pub algorithm: String,
pub signature: String,
pub pubkey: String,
}
impl PqcSignature {
pub fn new(
algorithm: impl Into<String>, signature: impl Into<String>, pubkey: impl Into<String>,
) -> Self {
Self {
algorithm: algorithm.into(),
signature: signature.into(),
pubkey: pubkey.into(),
}
}
pub fn ml_dsa_65(signature: impl Into<String>, pubkey: impl Into<String>) -> Self {
Self::new("ML-DSA-65", signature, pubkey)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct ExtendedMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub classes_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub composition_strategy: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub installed_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub custom: BTreeMap<String, String>,
}
impl ExtendedMetadata {
pub fn is_empty(&self) -> bool {
self.namespace.is_none()
&& self.classes_count.is_none()
&& self.properties_count.is_none()
&& self.composition_strategy.is_none()
&& self.installed_at.is_none()
&& self.custom.is_empty()
}
pub fn ontology(namespace: impl Into<String>, classes: usize, properties: usize) -> Self {
Self {
namespace: Some(namespace.into()),
classes_count: Some(classes),
properties_count: Some(properties),
..Default::default()
}
}
}
impl From<crate::lockfile::LockEntry> for UnifiedLockEntry {
fn from(entry: crate::lockfile::LockEntry) -> Self {
Self {
id: entry.id.clone(),
version: entry.version,
integrity: entry.sha256,
source: LockSource::Git {
url: entry.source,
branch: None,
tag: None,
commit: None,
},
locked_at: Utc::now(),
dependencies: entry.dependencies.unwrap_or_default(),
metadata: ExtendedMetadata::default(),
pqc: entry.pqc_signature.map(|sig| PqcSignature {
algorithm: "Dilithium3".to_string(),
signature: sig,
pubkey: entry.pqc_pubkey.unwrap_or_default(),
}),
}
}
}
impl From<crate::packs::lockfile::LockedPack> for UnifiedLockEntry {
fn from(pack: crate::packs::lockfile::LockedPack) -> Self {
Self {
id: String::new(), version: pack.version,
integrity: pack.integrity.unwrap_or_default(),
source: match pack.source {
crate::packs::lockfile::PackSource::Registry { url } => LockSource::Registry {
url,
resolved: None,
},
crate::packs::lockfile::PackSource::GitHub { org, repo, branch } => {
LockSource::GitHub { org, repo, branch }
}
crate::packs::lockfile::PackSource::Local { path } => LockSource::Local { path },
},
locked_at: pack.installed_at,
dependencies: pack.dependencies,
metadata: ExtendedMetadata {
installed_at: Some(pack.installed_at),
..Default::default()
},
pqc: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unified_entry_creation() {
let entry = UnifiedLockEntry::new(
"io.ggen.test",
"1.0.0",
"abc123def456",
LockSource::Registry {
url: "https://registry.ggen.io".into(),
resolved: None,
},
);
assert_eq!(entry.version, "1.0.0");
assert_eq!(entry.integrity, "abc123def456");
}
#[test]
fn test_lock_source_display() {
let registry = LockSource::Registry {
url: "https://registry.ggen.io".into(),
resolved: None,
};
assert_eq!(registry.to_string(), "registry+https://registry.ggen.io");
let github = LockSource::GitHub {
org: "seanchatmangpt".into(),
repo: "ggen".into(),
branch: "main".into(),
};
assert_eq!(github.to_string(), "github:seanchatmangpt/ggen#main");
}
#[test]
fn test_extended_metadata_is_empty() {
let empty = ExtendedMetadata::default();
assert!(empty.is_empty());
let with_namespace = ExtendedMetadata {
namespace: Some("https://schema.org/".into()),
..Default::default()
};
assert!(!with_namespace.is_empty());
}
#[test]
fn test_pqc_signature_creation() {
let sig = PqcSignature::ml_dsa_65("base64sig", "base64pubkey");
assert_eq!(sig.algorithm, "ML-DSA-65");
}
}