datacard-rs 1.0.0

Generic binary card format library with checksums and pluggable format traits
Documentation
//! Card metadata implementation

use crate::error::Result;
use serde::{Deserialize, Serialize};

/// Card metadata (stored as JSON)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CardMetadata {
    /// Document identifier (e.g., "std::vec::Vec")
    pub id: String,

    /// Compressed payload size in bytes
    pub compressed_size: u64,

    /// Original CML size before compression (optional)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub original_size: Option<u64>,

    /// CML profile (e.g., "code:api", "legal:constitution")
    #[serde(skip_serializing_if = "Option::is_none")]
    pub profile: Option<String>,

    /// Creation timestamp (Unix milliseconds)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub created: Option<u64>,

    /// BytePunch dictionary version used
    #[serde(skip_serializing_if = "Option::is_none")]
    pub dict_version: Option<String>,
}

impl CardMetadata {
    /// Create minimal metadata with just ID and compressed size
    pub fn new(id: impl Into<String>, compressed_size: u64) -> Self {
        Self {
            id: id.into(),
            compressed_size,
            original_size: None,
            profile: None,
            created: None,
            dict_version: None,
        }
    }

    /// Builder: set original size
    pub fn with_original_size(mut self, size: u64) -> Self {
        self.original_size = Some(size);
        self
    }

    /// Builder: set CML profile
    pub fn with_profile(mut self, profile: impl Into<String>) -> Self {
        self.profile = Some(profile.into());
        self
    }

    /// Builder: set creation timestamp
    pub fn with_timestamp(mut self, timestamp: u64) -> Self {
        self.created = Some(timestamp);
        self
    }

    /// Builder: set dictionary version
    pub fn with_dict_version(mut self, version: impl Into<String>) -> Self {
        self.dict_version = Some(version.into());
        self
    }

    /// Serialize to JSON bytes
    pub fn to_json(&self) -> Result<Vec<u8>> {
        Ok(serde_json::to_vec(self)?)
    }

    /// Deserialize from JSON bytes
    pub fn from_json(bytes: &[u8]) -> Result<Self> {
        Ok(serde_json::from_slice(bytes)?)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_metadata_new() {
        let metadata = CardMetadata::new("test::doc", 1234);
        assert_eq!(metadata.id, "test::doc");
        assert_eq!(metadata.compressed_size, 1234);
        assert_eq!(metadata.original_size, None);
    }

    #[test]
    fn test_metadata_builder() {
        let metadata = CardMetadata::new("test::doc", 1234)
            .with_original_size(5678)
            .with_profile("code:api")
            .with_timestamp(1703001234567)
            .with_dict_version("cml-core-v1");

        assert_eq!(metadata.original_size, Some(5678));
        assert_eq!(metadata.profile.as_deref(), Some("code:api"));
        assert_eq!(metadata.created, Some(1703001234567));
        assert_eq!(metadata.dict_version.as_deref(), Some("cml-core-v1"));
    }

    #[test]
    fn test_metadata_json_roundtrip() {
        let metadata = CardMetadata::new("test::doc", 1234)
            .with_original_size(5678)
            .with_profile("code:api")
            .with_timestamp(1703001234567);

        let json = metadata.to_json().unwrap();
        let loaded = CardMetadata::from_json(&json).unwrap();

        assert_eq!(loaded, metadata);
    }

    #[test]
    fn test_metadata_optional_fields() {
        let metadata = CardMetadata::new("minimal", 100);
        let json = metadata.to_json().unwrap();
        let json_str = String::from_utf8(json).unwrap();

        // Optional fields should be omitted
        assert!(!json_str.contains("original_size"));
        assert!(!json_str.contains("profile"));
        assert!(!json_str.contains("created"));
        assert!(!json_str.contains("dict_version"));
    }
}