alkstore 0.1.0

The alkstore contract crate: one reactive store trait surface — notify, streams, durable queues, TTL locks, outbox, scheduler — implemented by the alkstore-sqlite/-postgres/-mem engine crates; no driver dependencies
Documentation
use crate::validation::{
    NameKind, RESERVED_LISTENER_RECONNECTED, RESERVED_PREFIX, validate_local_name, validate_name,
    validate_shared_name,
};
use crate::{Error, Result};

fn assert_invalidname(r: Result<()>, expected: &str) {
    match r {
        Err(Error::InvalidName { name }) => assert_eq!(name, expected),
        other => panic!("expected InvalidName({expected}), got {other:?}"),
    }
}

fn assert_reservedname(r: Result<()>, expected: &str) {
    match r {
        Err(Error::ReservedName { name }) => assert_eq!(name, expected),
        other => panic!("expected ReservedName({expected}), got {other:?}"),
    }
}

#[test]
fn shared_name_accepts_legal_names() {
    for legal in [
        "orders",
        "a",
        "names with spaces",
        "__not_the_reserved_prefix",
        "_alkstore_x",
        "__alkstor_e_",
        "__ALKSTORE_lower",
        "uniße-名前",
    ] {
        validate_shared_name(legal).unwrap_or_else(|e| panic!("legal {legal:?} rejected: {e}"));
    }
}

#[test]
fn shared_name_rejects_empty() {
    assert_invalidname(validate_shared_name(""), "");
}

#[test]
fn shared_name_rejects_whitespace_only() {
    for ws in [" ", "\t", "\n", "  \t \n "] {
        assert_invalidname(validate_shared_name(ws), ws);
    }
}

#[test]
fn shared_name_rejects_reserved_prefix() {
    for reserved in [
        "__alkstore_",
        "__alkstore_outbox:foo",
        "__alkstore_listener_reconnected__",
    ] {
        assert_reservedname(validate_shared_name(reserved), reserved);
    }
}

#[test]
fn local_name_accepts_legal_names() {
    for legal in ["worker-1", "__alkstore_my_local_consumer"] {
        validate_local_name(legal).unwrap_or_else(|e| panic!("legal {legal:?} rejected: {e}"));
    }
}

#[test]
fn local_name_rejects_empty_and_whitespace_only() {
    assert_invalidname(validate_local_name(""), "");
    for ws in [" ", "\t\n "] {
        assert_invalidname(validate_local_name(ws), ws);
    }
}

#[test]
fn name_kind_dispatch_via_validate_name() {
    assert!(validate_name(NameKind::Shared, "__alkstore_x").is_err());
    assert!(validate_name(NameKind::Local, "__alkstore_x").is_ok());
    assert!(validate_name(NameKind::Shared, "ok").is_ok());
    assert!(validate_name(NameKind::Local, "ok").is_ok());
}

#[test]
fn error_display_texts() {
    let e = Error::PayloadTooLarge { limit: 8000 };
    assert!(e.to_string().contains("8000"));
    assert!(
        Error::ReservedName { name: "x".into() }
            .to_string()
            .contains("x")
    );
    assert!(
        Error::InvalidName { name: "y".into() }
            .to_string()
            .contains("y")
    );
    assert_eq!(
        Error::Closed.to_string(),
        "the receiver or source is closed"
    );
    assert!(
        Error::Codec("bad utf8 at 3".into())
            .to_string()
            .contains("bad utf8")
    );
    let db = std::io::Error::other("boom");
    let e = Error::Database(Box::new(db));
    assert!(e.to_string().contains("database"));
    let src = std::error::Error::source(&e).expect("Database carries #[source]");
    assert_eq!(src.to_string(), "boom");
}

#[test]
fn error_display_escapes_consumer_control_chars() {
    let forged = "in\u{1}ject\n\u{7}end\t";
    for e in [
        Error::InvalidName {
            name: forged.into(),
        },
        Error::ReservedName {
            name: forged.into(),
        },
        Error::InvalidSpec {
            spec: forged.into(),
        },
        Error::Codec(forged.into()),
    ] {
        let s = e.to_string();
        assert!(
            !s.chars().any(char::is_control),
            "raw control byte in rendered error: {s:?}"
        );
    }
    assert_eq!(
        Error::InvalidName {
            name: "x\u{1}\ny".into()
        }
        .to_string(),
        "invalid name `x\\u{1}\\ny`"
    );
    for plain in ["orders", "worker-1", "job row payload must be a string"] {
        assert!(
            Error::InvalidName { name: plain.into() }
                .to_string()
                .contains(plain)
        );
    }
}

#[test]
fn error_database_constructor() {
    let e = Error::database(std::io::Error::other("disk gone"));
    match e {
        Error::Database(source) => {
            assert_eq!(source.to_string(), "disk gone");
        }
        other => panic!("expected Database, got {other:?}"),
    }
}

#[test]
fn reserved_constants_exact() {
    assert_eq!(RESERVED_PREFIX, "__alkstore_");
    assert_eq!(
        RESERVED_LISTENER_RECONNECTED,
        "__alkstore_listener_reconnected__"
    );
}

#[test]
fn scheduler_error_variants() {
    let e = Error::InvalidSpec {
        spec: "0 0 * * *".into(),
    };
    assert!(e.to_string().contains("0 0 * * *"));
    assert_eq!(
        Error::LeadershipLost.to_string(),
        "scheduler leadership lost"
    );
}