use crate::config_lib::error::Result;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OntologyLockfile {
pub version: u32,
pub generated_at: String,
pub generator_version: String,
pub packages: BTreeMap<String, LockedPackage>,
pub composition: CompositionMetadata,
pub hashes: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LockedPackage {
pub version: String,
pub resolved: String,
pub integrity: String,
pub location: String,
pub namespace: Option<String>,
pub classes_count: usize,
pub properties_count: usize,
pub dependencies: BTreeMap<String, String>,
pub installed_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompositionMetadata {
pub strategy: String,
pub total_classes: usize,
pub total_properties: usize,
pub conflicts_resolved: usize,
pub validation_status: String,
}
pub struct LockfileManager;
impl LockfileManager {
pub fn new(_output_dir: &Path) -> Self {
Self
}
pub fn create(
packages: BTreeMap<String, LockedPackage>, composition: CompositionMetadata,
) -> Result<OntologyLockfile> {
let lockfile = OntologyLockfile {
version: 1,
generated_at: chrono::Utc::now().to_rfc3339(),
generator_version: env!("CARGO_PKG_VERSION").to_string(),
hashes: Self::compute_hashes(&packages),
packages,
composition,
};
lockfile.validate()?;
Ok(lockfile)
}
pub fn load(path: &Path) -> Result<OntologyLockfile> {
let content = std::fs::read_to_string(path).map_err(|e| {
crate::config_lib::ConfigError::Validation(format!("Failed to read lock file: {}", e))
})?;
let lockfile: OntologyLockfile = toml::from_str(&content).map_err(|e| {
crate::config_lib::ConfigError::Validation(format!("Failed to parse lock file: {}", e))
})?;
lockfile.validate()?;
Ok(lockfile)
}
pub fn save(lockfile: &OntologyLockfile, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
crate::config_lib::ConfigError::Validation(format!(
"Failed to create lock file directory: {}",
e
))
})?;
}
let content = toml::to_string_pretty(lockfile).map_err(|e| {
crate::config_lib::ConfigError::Validation(format!(
"Failed to serialize lock file: {}",
e
))
})?;
std::fs::write(path, content).map_err(|e| {
crate::config_lib::ConfigError::Validation(format!("Failed to write lock file: {}", e))
})?;
Ok(())
}
pub fn verify(lockfile: &OntologyLockfile) -> Result<()> {
let computed_hashes = Self::compute_hashes(&lockfile.packages);
for (name, expected_hash) in &lockfile.hashes {
match computed_hashes.get(name) {
Some(actual_hash) if actual_hash == expected_hash => continue,
Some(actual_hash) => {
return Err(crate::config_lib::ConfigError::Validation(format!(
"Lock file hash mismatch for '{}': expected {}, got {}",
name, expected_hash, actual_hash
)))
}
None => {
return Err(crate::config_lib::ConfigError::Validation(format!(
"Package '{}' in hash list not found in lock file",
name
)))
}
}
}
Ok(())
}
pub fn get_package<'a>(
lockfile: &'a OntologyLockfile, name: &str,
) -> Option<&'a LockedPackage> {
lockfile.packages.get(name)
}
pub fn all_packages(lockfile: &OntologyLockfile) -> Vec<(&str, &LockedPackage)> {
lockfile
.packages
.iter()
.map(|(k, v)| (k.as_str(), v))
.collect()
}
pub fn is_stale(lockfile: &OntologyLockfile, since: &str) -> bool {
lockfile.generated_at.as_str() < since
}
fn compute_hashes(packages: &BTreeMap<String, LockedPackage>) -> BTreeMap<String, String> {
let mut hashes = BTreeMap::new();
for (name, package) in packages {
let data = format!(
"{}:{}:{}:{}",
name,
package.version,
package.namespace.as_deref().unwrap_or(""),
package.integrity
);
let hash = format!("{:x}", Self::sha256(&data));
hashes.insert(name.clone(), hash);
}
hashes
}
fn sha256(data: &str) -> u64 {
let mut hash = 0u64;
for byte in data.as_bytes() {
hash = hash.wrapping_mul(31).wrapping_add(*byte as u64);
}
hash
}
}
impl OntologyLockfile {
pub fn validate(&self) -> Result<()> {
if self.version != 1 {
return Err(crate::config_lib::ConfigError::Validation(format!(
"Unsupported lock file version: {}",
self.version
)));
}
if self.packages.is_empty() {
return Err(crate::config_lib::ConfigError::Validation(
"Lock file contains no packages".to_string(),
));
}
for (name, package) in &self.packages {
if package.version.is_empty() {
return Err(crate::config_lib::ConfigError::Validation(format!(
"Package '{}' has no version",
name
)));
}
if package.integrity.is_empty() {
return Err(crate::config_lib::ConfigError::Validation(format!(
"Package '{}' has no integrity hash",
name
)));
}
}
if self.composition.strategy.is_empty() {
return Err(crate::config_lib::ConfigError::Validation(
"Composition strategy not specified".to_string(),
));
}
Ok(())
}
pub fn to_version_spec(&self) -> BTreeMap<String, String> {
self.packages
.iter()
.map(|(name, pkg)| (name.clone(), pkg.version.clone()))
.collect()
}
pub fn to_env_vars(&self) -> BTreeMap<String, String> {
let mut vars = BTreeMap::new();
for (name, package) in &self.packages {
let env_name = name.to_uppercase().replace('-', "_");
vars.insert(
format!("GGEN_PACK_{}_VERSION", env_name),
package.version.clone(),
);
vars.insert(
format!("GGEN_PACK_{}_INTEGRITY", env_name),
package.integrity.clone(),
);
}
vars.insert(
"GGEN_LOCK_GENERATED_AT".to_string(),
self.generated_at.clone(),
);
vars
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
fn create_test_lockfile() -> OntologyLockfile {
let mut packages = BTreeMap::new();
packages.insert(
"schema-org".to_string(),
LockedPackage {
version: "3.13.0".to_string(),
resolved: "registry://ggen-marketplace/schema-org@3.13.0".to_string(),
integrity: "sha256-abc123def456".to_string(),
location: ".ggen/packages/schema-org/3.13.0".to_string(),
namespace: Some("https://schema.org/".to_string()),
classes_count: 788,
properties_count: 2500,
dependencies: BTreeMap::new(),
installed_at: chrono::Utc::now().to_rfc3339(),
},
);
OntologyLockfile {
version: 1,
generated_at: chrono::Utc::now().to_rfc3339(),
generator_version: "3.2.0".to_string(),
packages,
composition: CompositionMetadata {
strategy: "union".to_string(),
total_classes: 788,
total_properties: 2500,
conflicts_resolved: 0,
validation_status: "valid".to_string(),
},
hashes: BTreeMap::new(),
}
}
#[test]
fn test_lockfile_creation() {
let lockfile = create_test_lockfile();
assert!(lockfile.validate().is_ok());
assert_eq!(lockfile.version, 1);
assert_eq!(lockfile.packages.len(), 1);
}
#[test]
fn test_lockfile_to_version_spec() {
let lockfile = create_test_lockfile();
let spec = lockfile.to_version_spec();
assert_eq!(spec.get("schema-org"), Some(&"3.13.0".to_string()));
}
#[test]
fn test_lockfile_to_env_vars() {
let lockfile = create_test_lockfile();
let vars = lockfile.to_env_vars();
assert!(vars.contains_key("GGEN_PACK_SCHEMA_ORG_VERSION"));
assert!(vars.contains_key("GGEN_LOCK_GENERATED_AT"));
}
#[test]
fn test_lockfile_manager_verify() {
let lockfile = create_test_lockfile();
assert!(lockfile.validate().is_ok());
}
}