use std::sync::atomic::{AtomicU64, Ordering};
use crate::time::TimeSource;
const COUNTER_MASK: u64 = 0xFFFF_FFFF_FFFF;
pub struct IdGenerator {
counter: AtomicU64,
seed: u32,
}
impl Default for IdGenerator {
fn default() -> Self {
Self::new()
}
}
impl IdGenerator {
#[must_use]
pub fn new() -> Self {
Self {
counter: AtomicU64::new(0),
seed: rand::random(),
}
}
#[must_use]
pub fn with_seed(seed: u32) -> Self {
Self {
counter: AtomicU64::new(0),
seed,
}
}
#[must_use]
#[expect(clippy::cast_possible_truncation, reason = "clamped to u64::MAX before cast")]
pub fn generate(&self, time_source: &dyn TimeSource) -> String {
let micros = time_source.now().as_micros().min(u128::from(u64::MAX)) as u64;
let micros_masked = micros & COUNTER_MASK;
let seq = self.counter.fetch_add(1, Ordering::Relaxed) & COUNTER_MASK;
format!("{micros_masked:012x}{:08x}{seq:012x}", self.seed)
}
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(clippy::unwrap_used, reason = "tests")]
mod tests {
use std::time::Duration;
use super::*;
use crate::time::FixedTimeSource;
#[test]
fn deterministic_output_with_fixed_seed() {
let generator = IdGenerator::with_seed(0xDEAD_BEEF);
let ts = FixedTimeSource::new(Duration::from_micros(0x0011_2233_4455));
let id = generator.generate(&ts);
assert_eq!(
id, "001122334455deadbeef000000000000",
"fixed seed + fixed time should produce a known ID"
);
}
#[test]
fn consecutive_ids_differ() {
let generator = IdGenerator::with_seed(0);
let ts = FixedTimeSource::new(Duration::from_secs(1_700_000_000));
let id1 = generator.generate(&ts);
let id2 = generator.generate(&ts);
assert_ne!(id1, id2, "consecutive IDs must differ");
}
#[test]
fn ids_are_32_chars() {
let generator = IdGenerator::with_seed(0);
let ts = FixedTimeSource::new(Duration::from_secs(1_700_000_000));
let id = generator.generate(&ts);
assert_eq!(id.len(), 32, "ID must be 32 hex characters");
}
#[test]
fn different_seeds_produce_different_ids() {
let ts = FixedTimeSource::new(Duration::from_secs(1_700_000_000));
let generator_a = IdGenerator::with_seed(1);
let generator_b = IdGenerator::with_seed(2);
let id_a = generator_a.generate(&ts);
let id_b = generator_b.generate(&ts);
assert_ne!(
id_a, id_b,
"same timestamp + same counter but different seeds must differ"
);
}
#[test]
fn counter_masks_to_48_bits() {
let generator = IdGenerator::with_seed(0);
let ts = FixedTimeSource::new(Duration::from_secs(0));
generator.counter.store(COUNTER_MASK, Ordering::Relaxed);
let id = generator.generate(&ts);
assert!(
id.ends_with("ffffffffffff"),
"counter at 48-bit max should show 12 hex f's, got: {id}"
);
let id_next = generator.generate(&ts);
assert!(
id_next.ends_with("000000000000"),
"counter past 48-bit max should wrap to zero, got: {id_next}"
);
}
#[test]
fn new_creates_with_random_seed() {
let generator = IdGenerator::new();
let ts = FixedTimeSource::new(Duration::from_secs(1_700_000_000));
let id = generator.generate(&ts);
assert_eq!(id.len(), 32, "new() generator should produce 32-char IDs");
}
}