winbrew_models\catalog/
metadata.rs

1//! Catalog metadata summary used to index and version the generated catalog.
2
3use std::collections::BTreeMap;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use serde::{Deserialize, Serialize};
7
8use crate::shared::ModelError;
9
10pub const SCHEMA_VERSION: u32 = 1;
11/// Schema version for the generated SQLite catalog database.
12pub const CATALOG_DB_SCHEMA_VERSION: u32 = 2;
13
14/// Summary metadata for a generated catalog snapshot.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct CatalogMetadata {
17    /// Schema version of the persisted metadata envelope.
18    pub schema_version: u32,
19    /// Unix timestamp captured when the metadata was generated.
20    pub generated_at_unix: u64,
21    /// Hash of the current catalog payload.
22    pub current_hash: String,
23    /// Hash of the previous catalog payload, when known.
24    pub previous_hash: String,
25    /// Total package count in the snapshot.
26    pub package_count: usize,
27    /// Number of packages by source name.
28    pub source_counts: BTreeMap<String, usize>,
29}
30
31impl CatalogMetadata {
32    /// Build metadata from aggregate counts and the current payload hash.
33    pub fn build_from_counts(
34        package_count: usize,
35        source_counts: BTreeMap<String, usize>,
36        current_hash: String,
37    ) -> Self {
38        Self {
39            schema_version: SCHEMA_VERSION,
40            generated_at_unix: SystemTime::now()
41                .duration_since(UNIX_EPOCH)
42                .unwrap_or_default()
43                .as_secs(),
44            current_hash,
45            previous_hash: String::default(),
46            package_count,
47            source_counts,
48        }
49    }
50
51    /// Validate the schema version and the required hash contract.
52    pub fn validate(&self) -> Result<(), ModelError> {
53        if self.schema_version != SCHEMA_VERSION {
54            return Err(ModelError::invalid_contract(
55                "catalog_metadata.schema_version",
56                format!(
57                    "unsupported catalog metadata schema version: expected {SCHEMA_VERSION}, got {}",
58                    self.schema_version
59                ),
60            ));
61        }
62
63        if self.current_hash.trim().is_empty() {
64            return Err(ModelError::invalid_contract(
65                "catalog_metadata.current_hash",
66                "current_hash cannot be empty",
67            ));
68        }
69
70        Ok(())
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::CatalogMetadata;
77    use std::collections::BTreeMap;
78
79    #[test]
80    fn builds_metadata_with_schema_version() {
81        let metadata = CatalogMetadata::build_from_counts(
82            2,
83            BTreeMap::from([(String::from("scoop"), 1)]),
84            String::from("sha256:abc"),
85        );
86
87        assert_eq!(metadata.schema_version, 1);
88        assert_eq!(metadata.package_count, 2);
89        assert_eq!(metadata.source_counts.get("scoop"), Some(&1));
90    }
91}