synapse-primitives 0.0.2

Core types and ID hashing for Synapse RPC framework
Documentation
//! SipHash-based name-to-ID mapping for stable numeric identifiers
//!
//! This module provides utilities to hash string names into stable 32-bit or 64-bit
//! numeric IDs using SipHash-2-4. This is useful for converting human-readable names
//! into efficient numeric identifiers for wire protocols.
//!
//! # Examples
//!
//! ```
//! use synapse_primitives::siphash::hash_name_u32;
//!
//! // Interface IDs from fully-qualified names
//! let interface_id = hash_name_u32("mensa.user.v2.UserInterface");
//! let another_id = hash_name_u32("mensa.payment.v1.PaymentInterface");
//!
//! // Header key IDs from header names
//! let trace_id_key = hash_name_u32("trace_id");
//! let request_id_key = hash_name_u32("request_id");
//! ```

use siphasher::sip::SipHasher24;
use std::hash::{Hash, Hasher};

/// Default SipHash key for name hashing (k0, k1)
/// Using non-zero constants to avoid trivial collisions
const DEFAULT_SIPHASH_KEY: (u64, u64) = (
    0x0706050403020100, // k0
    0x0F0E0D0C0B0A0908, // k1
);

/// Hash a name to a 32-bit identifier using SipHash-2-4
///
/// This provides a stable, deterministic mapping from strings to u32 IDs.
/// The same name will always produce the same ID across different runs and systems.
///
/// # Examples
///
/// ```
/// use synapse_primitives::siphash::hash_name_u32;
///
/// let id = hash_name_u32("mensa.user.v2.UserInterface");
/// assert_eq!(id, hash_name_u32("mensa.user.v2.UserInterface")); // Deterministic
/// ```
pub fn hash_name_u32(name: &str) -> u32 {
    let hash = hash_name_u64(name);
    // XOR-fold 64-bit hash into 32-bit to preserve entropy
    ((hash >> 32) ^ hash) as u32
}

/// Hash a name to a 64-bit identifier using SipHash-2-4
///
/// Provides more bits than u32 variant, reducing collision probability
/// for applications that need more ID space.
///
/// # Examples
///
/// ```
/// use synapse_primitives::siphash::hash_name_u64;
///
/// let id = hash_name_u64("request_count");
/// ```
pub fn hash_name_u64(name: &str) -> u64 {
    let mut hasher = SipHasher24::new_with_keys(DEFAULT_SIPHASH_KEY.0, DEFAULT_SIPHASH_KEY.1);
    name.hash(&mut hasher);
    hasher.finish()
}

/// Hash a name with a custom SipHash key
///
/// Useful when you need isolated namespaces or want to avoid collisions
/// with the default key. The custom key must be kept consistent across
/// systems for deterministic results.
///
/// # Examples
///
/// ```
/// use synapse_primitives::siphash::hash_name_with_key;
///
/// let custom_key = (0x1234567890ABCDEF, 0xFEDCBA0987654321);
/// let id = hash_name_with_key("custom_namespace::item", custom_key);
/// ```
pub fn hash_name_with_key(name: &str, key: (u64, u64)) -> u64 {
    let mut hasher = SipHasher24::new_with_keys(key.0, key.1);
    name.hash(&mut hasher);
    hasher.finish()
}

/// Macro to compute interface ID
///
/// This is useful for defining interface IDs in your code.
///
/// # Examples
///
/// ```
/// use synapse_primitives::interface_id;
///
/// let user_interface_id: u32 = interface_id!("mensa.user.v2.UserInterface");
/// ```
#[macro_export]
macro_rules! interface_id {
    ($name:expr) => {{ $crate::siphash::hash_name_u32($name) }};
}

/// Macro to compute header key ID
///
/// # Examples
///
/// ```
/// use synapse_primitives::header_key_id;
///
/// let trace_id_key: u32 = header_key_id!("trace_id");
/// ```
#[macro_export]
macro_rules! header_key_id {
    ($name:expr) => {{ $crate::siphash::hash_name_u32($name) }};
}

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

    #[test]
    fn test_deterministic_hashing() {
        let name = "mensa.user.v2.UserInterface";
        let id1 = hash_name_u32(name);
        let id2 = hash_name_u32(name);
        assert_eq!(id1, id2, "Same name should produce same hash");
    }

    #[test]
    fn test_different_names_different_hashes() {
        let id1 = hash_name_u32("mensa.user.v2.UserInterface");
        let id2 = hash_name_u32("mensa.payment.v1.PaymentInterface");
        assert_ne!(id1, id2, "Different names should produce different hashes");
    }

    #[test]
    fn test_u64_vs_u32() {
        let name = "test_interface";
        let hash64 = hash_name_u64(name);
        let hash32 = hash_name_u32(name);

        // u32 should be XOR-fold of u64
        let expected_u32 = ((hash64 >> 32) ^ hash64) as u32;
        assert_eq!(hash32, expected_u32);
    }

    #[test]
    fn test_custom_key() {
        let name = "test_name";
        let default_hash = hash_name_u64(name);
        let custom_hash = hash_name_with_key(name, (0x1111111111111111, 0x2222222222222222));

        assert_ne!(
            default_hash, custom_hash,
            "Custom key should produce different hash"
        );
    }

    #[test]
    fn test_header_keys() {
        let trace_id = hash_name_u32("trace_id");
        let request_id = hash_name_u32("request_id");
        let span_id = hash_name_u32("span_id");

        // All should be different
        assert_ne!(trace_id, request_id);
        assert_ne!(trace_id, span_id);
        assert_ne!(request_id, span_id);
    }

    #[test]
    fn test_version_sensitivity() {
        let v1 = hash_name_u32("mensa.user.v1.UserInterface");
        let v2 = hash_name_u32("mensa.user.v2.UserInterface");

        assert_ne!(v1, v2, "Different versions should have different IDs");
    }
}