guardian-db 0.19.0

High-performance, local-first decentralized database built on Rust and Iroh
Documentation
use crate::guardian::error::{GuardianError, Result};
use crate::guardian::serializer;
use crate::log::entry::Entry;
use serde::{Deserialize, Serialize};

pub mod traits;

/// Represents a document within a log operation.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OpDoc {
    #[serde(rename = "key")]
    pub key: String,

    #[serde(rename = "value")]
    pub value: Vec<u8>,
}

impl OpDoc {
    /// Creates a new OpDoc.
    pub fn new(key: String, value: Vec<u8>) -> Self {
        Self { key, value }
    }

    /// Getter for key.
    pub fn key(&self) -> &str {
        &self.key
    }

    /// Getter for value.
    pub fn value(&self) -> &[u8] {
        &self.value
    }
}

/// Represents an operation to be added to the log.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Operation {
    #[serde(rename = "key")]
    pub key: Option<String>,

    #[serde(rename = "op")]
    pub op: String,

    #[serde(rename = "value", default)]
    pub value: Vec<u8>,

    #[serde(rename = "docs", default)]
    pub docs: Vec<OpDoc>,

    #[serde(skip)]
    pub entry: Option<Entry>, // Changed to Option to indicate when it is not available.
}

impl Operation {
    /// Creates a new basic operation.
    pub fn new(key: Option<String>, op: String, value: Option<Vec<u8>>) -> Self {
        Self {
            key,
            op,
            value: value.unwrap_or_default(),
            docs: Vec::new(),
            entry: None,
        }
    }

    /// Creates a new operation with documents (for batch operations).
    pub fn new_with_documents(
        key: Option<String>,
        op: String,
        docs: Vec<(String, Vec<u8>)>,
    ) -> Self {
        let docs = docs.into_iter().map(|(k, v)| OpDoc::new(k, v)).collect();
        Self {
            key,
            op,
            value: Vec::new(),
            docs,
            entry: None,
        }
    }

    /// Public getters to access the fields.
    pub fn key(&self) -> Option<&String> {
        self.key.as_ref()
    }

    pub fn op(&self) -> &str {
        &self.op
    }

    pub fn value(&self) -> &[u8] {
        &self.value
    }

    pub fn docs(&self) -> &[OpDoc] {
        &self.docs
    }

    pub fn entry(&self) -> Option<&Entry> {
        self.entry.as_ref()
    }

    /// Sets the entry after the operation is created.
    pub fn set_entry(&mut self, entry: Entry) {
        self.entry = Some(entry);
    }

    /// Checks whether the operation has a valid entry.
    pub fn has_entry(&self) -> bool {
        self.entry.is_some()
    }

    /// Serializes the operation to bytes using postcard.
    pub fn marshal(&self) -> Result<Vec<u8>> {
        serializer::serialize(self)
    }
}

/// Parses a Log Entry to extract the operation data.
///
/// **NOTE:** Entry.payload is now Vec<u8> (migrated in Phase 3).
pub fn parse_operation(entry: Entry) -> Result<Operation> {
    // The payload contains base64-encoded data; decode it first.
    let payload_bytes = entry.payload();

    // Decode base64 to get the original bytes from the Postcard serialization.
    use base64::{Engine as _, engine::general_purpose};
    let decoded_bytes = general_purpose::STANDARD
        .decode(payload_bytes)
        .map_err(|err| GuardianError::Store(format!("Unable to decode base64 payload: {}", err)))?;

    // Deserialize the payload using postcard.
    let mut op: Operation = serializer::deserialize(&decoded_bytes).map_err(|err| {
        GuardianError::Store(format!("Unable to parse operation payload: {}", err))
    })?;

    // Attach the entry to the operation after successful deserialization.
    op.entry = Some(entry);

    Ok(op)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::log::identity::Identity;

    fn create_test_identity() -> Identity {
        let signatures = crate::log::identity::Signatures::new("test_id_sign", "test_pub_sign");
        Identity::new("test_peer", "test_pub_key", signatures)
    }

    #[test]
    fn test_op_doc_creation() {
        let doc = OpDoc::new("test_key".to_string(), b"test_value".to_vec());
        assert_eq!(doc.key(), "test_key");
        assert_eq!(doc.value(), b"test_value");
    }

    #[test]
    fn test_operation_new() {
        let op = Operation::new(
            Some("test_key".to_string()),
            "PUT".to_string(),
            Some(b"test_data".to_vec()),
        );

        assert_eq!(op.key(), Some(&"test_key".to_string()));
        assert_eq!(op.op(), "PUT");
        assert_eq!(op.value(), b"test_data");
        assert!(op.docs().is_empty());
        assert!(!op.has_entry());
    }

    #[test]
    fn test_operation_with_documents() {
        let docs = vec![
            ("key1".to_string(), b"value1".to_vec()),
            ("key2".to_string(), b"value2".to_vec()),
        ];

        let op = Operation::new_with_documents(None, "PUTALL".to_string(), docs);

        assert_eq!(op.key(), None);
        assert_eq!(op.op(), "PUTALL");
        assert!(op.value().is_empty());
        assert_eq!(op.docs().len(), 2);
        assert_eq!(op.docs()[0].key(), "key1");
        assert_eq!(op.docs()[1].key(), "key2");
    }

    #[test]
    fn test_operation_marshal() {
        let op = Operation::new(
            Some("test".to_string()),
            "PUT".to_string(),
            Some(b"value".to_vec()),
        );

        let bytes = op.marshal().expect("Failed to marshal operation");

        // Postcard is binary, so we just check that it is not empty.
        assert!(!bytes.is_empty());
        println!("✓ Marshaled operation: {} bytes", bytes.len());
    }

    #[test]
    fn test_operation_marshal_determinism() {
        let op = Operation::new(
            Some("determinism_test".to_string()),
            "PUT".to_string(),
            Some(b"test_data".to_vec()),
        );

        // Serialize 3 times.
        let hashes: Vec<String> = (0..3)
            .map(|_| {
                let bytes = op.marshal().expect("Marshal failed");
                blake3::hash(&bytes).to_hex().to_string()
            })
            .collect();

        // All hashes must be identical.
        let first = &hashes[0];
        for hash in &hashes[1..] {
            assert_eq!(first, hash, "Operation serialization not deterministic!");
        }

        println!("✓ Operation deterministic hash: {}", first);
    }

    #[test]
    fn test_operation_set_entry() {
        let mut op = Operation::new(
            Some("test".to_string()),
            "PUT".to_string(),
            Some(b"value".to_vec()),
        );

        let identity = create_test_identity();
        let entry = Entry::new(identity, "test_log", b"test_payload", &[], None);

        op.set_entry(entry);
        assert!(op.has_entry());

        println!("✓ Operation set_entry successful");
    }

    #[test]
    fn test_operation_roundtrip() {
        let original = Operation::new_with_documents(
            Some("batch_key".to_string()),
            "PUTALL".to_string(),
            vec![
                ("doc1".to_string(), b"value1".to_vec()),
                ("doc2".to_string(), b"value2".to_vec()),
                ("doc3".to_string(), b"value3".to_vec()),
            ],
        );

        // Serialize.
        let bytes = original.marshal().expect("Marshal failed");

        // Deserialize.
        let deserialized: Operation = serializer::deserialize(&bytes).expect("Deserialize failed");

        // Validate.
        assert_eq!(original.key(), deserialized.key());
        assert_eq!(original.op(), deserialized.op());
        assert_eq!(original.docs().len(), deserialized.docs().len());

        for (orig_doc, deser_doc) in original.docs().iter().zip(deserialized.docs().iter()) {
            assert_eq!(orig_doc.key(), deser_doc.key());
            assert_eq!(orig_doc.value(), deser_doc.value());
        }

        println!("✓ Complex operation roundtrip successful");
    }

    #[test]
    fn test_parse_operation() {
        // Create an operation and serialize it using postcard.
        let original_op = Operation::new(
            Some("parse_test".to_string()),
            "GET".to_string(),
            Some(b"parse_data".to_vec()),
        );

        let postcard_data = original_op.marshal().expect("Failed to marshal");

        // Encode to base64 as add_operation does.
        use base64::{Engine as _, engine::general_purpose};
        let base64_data = general_purpose::STANDARD.encode(&postcard_data);

        // Create an entry with the base64 payload.
        let identity = create_test_identity();
        let entry = Entry::new(identity, "test_log", base64_data.as_bytes(), &[], None);

        // Test the parse.
        let parsed_op = parse_operation(entry).expect("Failed to parse operation");

        assert_eq!(parsed_op.key(), Some(&"parse_test".to_string()));
        assert_eq!(parsed_op.op(), "GET");
        assert_eq!(parsed_op.value(), b"parse_data");
        assert!(parsed_op.has_entry());
    }
}