herolib-sid 0.1.0

SmartID - Short, human-readable, collision-free identifiers
Documentation
//! Contributor management for SmartID spaces

use super::error::{Result, SidError};
use super::smartid::SmartId;
use serde::{Deserialize, Serialize};

/// Maximum number of contributors per space
pub const MAX_CONTRIBUTORS: u16 = 999;

/// A contributor within a SmartID space.
///
/// Each contributor has:
/// - A unique ID (0-998) assigned by HeroLedger
/// - A local counter for minting new IDs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contributor {
    cid: u16,
    local_counter: u64,
}

impl Contributor {
    /// Create a new contributor with a given ID.
    ///
    /// The local counter starts at 0.
    pub fn new(cid: u16) -> Result<Self> {
        if cid >= MAX_CONTRIBUTORS {
            return Err(SidError::ContributorIdTooLarge(cid));
        }
        Ok(Self {
            cid,
            local_counter: 0,
        })
    }

    /// Create a contributor with an existing counter value.
    ///
    /// Used when loading from storage.
    pub fn with_counter(cid: u16, local_counter: u64) -> Result<Self> {
        if cid >= MAX_CONTRIBUTORS {
            return Err(SidError::ContributorIdTooLarge(cid));
        }
        Ok(Self { cid, local_counter })
    }

    /// Get the contributor ID.
    pub fn cid(&self) -> u16 {
        self.cid
    }

    /// Get the current local counter value.
    pub fn local_counter(&self) -> u64 {
        self.local_counter
    }

    /// Mint a new SmartID for this contributor.
    ///
    /// This increments the local counter and returns a new SmartID.
    pub fn mint(&mut self, num_contributors: u16, min_length: usize) -> Result<SmartId> {
        let sid = SmartId::compute(self.local_counter, self.cid, num_contributors, min_length)?;
        self.local_counter += 1;
        Ok(sid)
    }

    /// Peek at the next SmartID without incrementing the counter.
    pub fn peek_next(&self, num_contributors: u16, min_length: usize) -> Result<SmartId> {
        SmartId::compute(self.local_counter, self.cid, num_contributors, min_length)
    }
}

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

    #[test]
    fn test_new_contributor() {
        let contrib = Contributor::new(0).unwrap();
        assert_eq!(contrib.cid(), 0);
        assert_eq!(contrib.local_counter(), 0);

        let contrib = Contributor::new(998).unwrap();
        assert_eq!(contrib.cid(), 998);
    }

    #[test]
    fn test_contributor_id_too_large() {
        let result = Contributor::new(999);
        assert!(matches!(result, Err(SidError::ContributorIdTooLarge(999))));

        let result = Contributor::new(1000);
        assert!(matches!(result, Err(SidError::ContributorIdTooLarge(1000))));
    }

    #[test]
    fn test_with_counter() {
        let contrib = Contributor::with_counter(5, 100).unwrap();
        assert_eq!(contrib.cid(), 5);
        assert_eq!(contrib.local_counter(), 100);
    }

    #[test]
    fn test_mint() {
        let mut contrib = Contributor::new(0).unwrap();

        // First ID is 1, not 0 (formula: local_counter * num_contributors + cid + 1)
        let sid1 = contrib.mint(1, 4).unwrap();
        assert_eq!(sid1.global_id(), 1); // 0 * 1 + 0 + 1 = 1
        assert_eq!(contrib.local_counter(), 1);

        let sid2 = contrib.mint(1, 4).unwrap();
        assert_eq!(sid2.global_id(), 2); // 1 * 1 + 0 + 1 = 2
        assert_eq!(contrib.local_counter(), 2);
    }

    #[test]
    fn test_mint_multi_contributor() {
        let mut contrib = Contributor::new(5).unwrap();

        // Formula: local_counter * num_contributors + cid + 1
        let sid = contrib.mint(10, 4).unwrap();
        assert_eq!(sid.global_id(), 0 * 10 + 5 + 1); // = 6

        let sid = contrib.mint(10, 4).unwrap();
        assert_eq!(sid.global_id(), 1 * 10 + 5 + 1); // = 16

        let sid = contrib.mint(10, 4).unwrap();
        assert_eq!(sid.global_id(), 2 * 10 + 5 + 1); // = 26
    }

    #[test]
    fn test_peek_next() {
        let contrib = Contributor::with_counter(3, 10).unwrap();

        // Formula: local_counter * num_contributors + cid + 1
        let sid = contrib.peek_next(100, 4).unwrap();
        assert_eq!(sid.global_id(), 10 * 100 + 3 + 1); // = 1004
        assert_eq!(contrib.local_counter(), 10); // peek doesn't increment
    }

    #[test]
    fn test_serde() {
        let contrib = Contributor::with_counter(42, 1000).unwrap();
        let json = serde_json::to_string(&contrib).unwrap();
        let parsed: Contributor = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.cid(), 42);
        assert_eq!(parsed.local_counter(), 1000);
    }
}