odoid 1.0.2

Deterministic mixed-radix ID encoding — maps integers to 6, 7, or 8-character alphanumeric strings. Ambiguous characters I, L, O are excluded.
Documentation
use odoid::{decode, GeneratorConfig, OdoIDGenerator, MAX};

#[test]
fn default_config() {
    let g = OdoIDGenerator::new(GeneratorConfig::default()).unwrap();
    assert_eq!(g.namespace, "default");
    assert_eq!(g.length, 6);
}

#[test]
fn custom_config() {
    let g = OdoIDGenerator::new(GeneratorConfig {
        namespace: "acme".into(),
        length: 8,
        ..Default::default()
    })
    .unwrap();
    assert_eq!(g.namespace, "acme");
    assert_eq!(g.length, 8);
    assert_eq!(g.capacity, MAX[8]);
}

#[test]
fn unsupported_length_errors() {
    assert!(OdoIDGenerator::new(GeneratorConfig { length: 5, ..Default::default() }).is_err());
}

#[test]
fn next_result_has_correct_shape() {
    let mut g = OdoIDGenerator::new(GeneratorConfig {
        namespace: "test".into(),
        length: 6,
        ..Default::default()
    })
    .unwrap();
    let r = g.next().unwrap();
    assert!(!r.id.is_empty());
    assert_eq!(r.length, 6);
    assert_eq!(r.namespace, "test");
}

#[test]
fn id_has_correct_length() {
    for &length in &[6, 7, 8] {
        let mut g = OdoIDGenerator::new(GeneratorConfig { length, ..Default::default() }).unwrap();
        assert_eq!(g.next().unwrap().id.len(), length);
    }
}

#[test]
fn excluded_chars_never_in_output() {
    let mut g = OdoIDGenerator::new(GeneratorConfig { length: 8, ..Default::default() }).unwrap();
    for _ in 0..50 {
        let id = g.next().unwrap().id;
        assert!(!id.contains('I'));
        assert!(!id.contains('L'));
        assert!(!id.contains('O'));
    }
}

#[test]
fn n_in_valid_range() {
    for &length in &[6, 7, 8] {
        let mut g = OdoIDGenerator::new(GeneratorConfig { length, ..Default::default() }).unwrap();
        for _ in 0..50 {
            let n = g.next().unwrap().n;
            assert!(n < MAX[length], "n={n} not < MAX[{length}]={}", MAX[length]);
        }
    }
}

#[test]
fn n_matches_decode_of_id() {
    let mut g = OdoIDGenerator::new(GeneratorConfig {
        namespace: "verify".into(),
        length: 6,
        ..Default::default()
    })
    .unwrap();
    for _ in 0..20 {
        let r = g.next().unwrap();
        assert_eq!(decode(&r.id).unwrap(), r.n);
    }
}

#[test]
fn monotonic_sequencing_same_tick_distinct_ids() {
    use std::time::{SystemTime, UNIX_EPOCH};
    let future_epoch = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_millis() as u64
        + 60_000;

    let mut g = OdoIDGenerator::new(GeneratorConfig {
        namespace: "seq-test".into(),
        epoch: Some(future_epoch),
        ..Default::default()
    })
    .unwrap();

    let ids: std::collections::HashSet<String> =
        (0..20).map(|_| g.next().unwrap().id).collect();
    assert_eq!(ids.len(), 20, "expected 20 distinct IDs, got {}", ids.len());
}

#[test]
fn namespace_isolation() {
    use std::time::{SystemTime, UNIX_EPOCH};
    let future_epoch = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_millis() as u64
        + 60_000;

    let mut g1 = OdoIDGenerator::new(GeneratorConfig {
        namespace: "ns-a".into(),
        length: 8,
        epoch: Some(future_epoch),
    })
    .unwrap();
    let mut g2 = OdoIDGenerator::new(GeneratorConfig {
        namespace: "ns-b".into(),
        length: 8,
        epoch: Some(future_epoch),
    })
    .unwrap();

    assert_ne!(g1.next().unwrap().id, g2.next().unwrap().id);
}

#[test]
fn proxy_encode_uses_generator_length() {
    let g = OdoIDGenerator::new(GeneratorConfig { length: 7, ..Default::default() }).unwrap();
    assert_eq!(g.encode(0).unwrap().len(), 7);
}

#[test]
fn proxy_decode_returns_correct_n() {
    let mut g = OdoIDGenerator::new(GeneratorConfig { length: 6, ..Default::default() }).unwrap();
    let r = g.next().unwrap();
    assert_eq!(g.decode(&r.id).unwrap(), r.n);
}