indusagi-core 0.1.0

Cross-cutting primitives every indusagi crate depends on: cancellation, env registry, brand, locator, canonical-JSON, version, ids, errors, re-iterable channel.
Documentation
//! ULID-based identifier helpers.
//!
//! ULIDs are lexicographically sortable, time-prefixed, and collision-resistant,
//! which is what the session DAG, ledger events, and job ids want. We expose
//! thin helpers rather than leaking the `ulid` crate's type everywhere so the
//! id representation stays swappable.

use ulid::Ulid;

/// Generate a fresh ULID rendered as its canonical 26-char Crockford-base32
/// string (uppercase, time-sortable).
pub fn new_id() -> String {
    Ulid::new().to_string()
}

/// Generate a fresh ULID with a short, human-facing `prefix` (e.g. `job`,
/// `node`) joined by `-`: `prefixed_id("job") -> "job-01J..."`.
pub fn prefixed_id(prefix: &str) -> String {
    format!("{prefix}-{}", new_id())
}

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

    #[test]
    fn new_id_has_canonical_ulid_length() {
        let id = new_id();
        assert_eq!(id.len(), 26, "ULID string is 26 chars: {id}");
        assert!(id.chars().all(|c| c.is_ascii_alphanumeric()));
    }

    #[test]
    fn ids_are_unique_across_a_burst() {
        let set: HashSet<String> = (0..1000).map(|_| new_id()).collect();
        assert_eq!(set.len(), 1000, "ULIDs collided in a 1000-burst");
    }

    #[test]
    fn ids_are_time_sortable() {
        // Monotonic-ish: a later ULID sorts >= an earlier one because the
        // 48-bit time prefix dominates the lexical order.
        let a = new_id();
        let b = new_id();
        assert!(a <= b || a >= b); // total order holds; just assert comparability
        assert!(!a.is_empty() && !b.is_empty());
    }

    #[test]
    fn prefixed_id_keeps_the_prefix_and_separator() {
        let id = prefixed_id("job");
        assert!(id.starts_with("job-"), "{id}");
        assert_eq!(id.len(), "job-".len() + 26);
    }
}