claw-vcs-core 0.1.1

Core object types and Claw Object Format encoding for Claw VCS.
Documentation
use data_encoding::BASE32_NOPAD;
use serde::{Deserialize, Serialize};
use std::fmt;
use ulid::Ulid;

use crate::CoreError;

const OBJECT_ID_PREFIX: &str = "clw_";

/// Content-addressed identifier for a stored Claw object.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ObjectId([u8; 32]);

impl ObjectId {
    /// Construct an object ID from raw 32-byte hash output.
    pub fn from_bytes(bytes: [u8; 32]) -> Self {
        Self(bytes)
    }

    /// Borrow the raw 32-byte hash output.
    pub fn as_bytes(&self) -> &[u8; 32] {
        &self.0
    }

    /// Format the ID as lowercase hexadecimal.
    pub fn to_hex(&self) -> String {
        hex::encode(self.0)
    }

    /// Parse a lowercase or uppercase hexadecimal object ID.
    pub fn from_hex(s: &str) -> Result<Self, CoreError> {
        let bytes = hex::decode(s).map_err(|e| CoreError::InvalidObjectId(e.to_string()))?;
        let arr: [u8; 32] = bytes
            .try_into()
            .map_err(|_| CoreError::InvalidObjectId("expected 32 bytes".into()))?;
        Ok(Self(arr))
    }

    /// Parse the human-facing `clw_` base32 display form.
    pub fn from_display(s: &str) -> Result<Self, CoreError> {
        let encoded = s.strip_prefix(OBJECT_ID_PREFIX).ok_or_else(|| {
            CoreError::InvalidObjectId(format!("missing prefix '{OBJECT_ID_PREFIX}'"))
        })?;
        let upper = encoded.to_uppercase();
        let bytes = BASE32_NOPAD
            .decode(upper.as_bytes())
            .map_err(|e| CoreError::InvalidObjectId(e.to_string()))?;
        let arr: [u8; 32] = bytes
            .try_into()
            .map_err(|_| CoreError::InvalidObjectId("expected 32 bytes".into()))?;
        Ok(Self(arr))
    }

    /// First 2 hex chars used for loose object directory sharding
    pub fn shard_prefix(&self) -> String {
        hex::encode(&self.0[..1])
    }

    /// Remaining hex chars for the loose object filename
    pub fn shard_suffix(&self) -> String {
        hex::encode(&self.0[1..])
    }
}

impl fmt::Display for ObjectId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let encoded = BASE32_NOPAD.encode(&self.0).to_lowercase();
        write!(f, "{OBJECT_ID_PREFIX}{encoded}")
    }
}

impl fmt::Debug for ObjectId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "ObjectId({})", self)
    }
}

/// Stable ULID identifier for an intent object.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct IntentId(Ulid);

impl IntentId {
    /// Generate a new time-sortable intent ID.
    pub fn new() -> Self {
        Self(Ulid::new())
    }

    /// Construct an intent ID from raw ULID bytes.
    pub fn from_bytes(bytes: [u8; 16]) -> Self {
        Self(Ulid::from_bytes(bytes))
    }

    /// Return the raw 16-byte ULID representation.
    pub fn as_bytes(&self) -> [u8; 16] {
        self.0.to_bytes()
    }

    /// Parse an intent ID from canonical ULID text.
    pub fn from_string(s: &str) -> Result<Self, CoreError> {
        let ulid = Ulid::from_string(s).map_err(|e| CoreError::InvalidObjectId(e.to_string()))?;
        Ok(Self(ulid))
    }
}

impl Default for IntentId {
    fn default() -> Self {
        Self::new()
    }
}

impl fmt::Display for IntentId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl fmt::Debug for IntentId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "IntentId({})", self.0)
    }
}

/// Stable ULID identifier for a change object.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ChangeId(Ulid);

impl ChangeId {
    /// Generate a new time-sortable change ID.
    pub fn new() -> Self {
        Self(Ulid::new())
    }

    /// Construct a change ID from raw ULID bytes.
    pub fn from_bytes(bytes: [u8; 16]) -> Self {
        Self(Ulid::from_bytes(bytes))
    }

    /// Return the raw 16-byte ULID representation.
    pub fn as_bytes(&self) -> [u8; 16] {
        self.0.to_bytes()
    }

    /// Parse a change ID from canonical ULID text.
    pub fn from_string(s: &str) -> Result<Self, CoreError> {
        let ulid = Ulid::from_string(s).map_err(|e| CoreError::InvalidObjectId(e.to_string()))?;
        Ok(Self(ulid))
    }
}

impl Default for ChangeId {
    fn default() -> Self {
        Self::new()
    }
}

impl fmt::Display for ChangeId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl fmt::Debug for ChangeId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "ChangeId({})", self.0)
    }
}

/// Stable ULID identifier for a conflict object.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ConflictId(Ulid);

impl ConflictId {
    /// Generate a new time-sortable conflict ID.
    pub fn new() -> Self {
        Self(Ulid::new())
    }

    /// Construct a conflict ID from raw ULID bytes.
    pub fn from_bytes(bytes: [u8; 16]) -> Self {
        Self(Ulid::from_bytes(bytes))
    }

    /// Return the raw 16-byte ULID representation.
    pub fn as_bytes(&self) -> [u8; 16] {
        self.0.to_bytes()
    }
}

impl Default for ConflictId {
    fn default() -> Self {
        Self::new()
    }
}

impl fmt::Display for ConflictId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl fmt::Debug for ConflictId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "ConflictId({})", self.0)
    }
}