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
//! Entry-point name validation (ADR-008 §4) and the reserved
//! namespace.
//!
//! Every name-bearing entry point validates before any engine round
//! trip — on the auto-commit and `*_tx` paths alike — via these
//! helpers.

/// The reserved namespace prefix (ADR-008 §4). Engine-derived names in
/// consumer namespaces (e.g. an outbox's backing queue) carry it;
/// consumer-supplied shared-namespace names carrying it are rejected
/// with [`Error::ReservedName`](crate::Error::ReservedName).
pub const RESERVED_PREFIX: &str = "__alkstore_";

/// The one v1-reserved string (ADR-008 §4): the Postgres engine's
/// synthetic reconnect-wake channel, delivered as
/// `Wake { channel: RESERVED_LISTENER_RECONNECTED }` on every
/// subscriber's receiver after a watcher reconnect. Public in core so
/// the pg engine and the contract suite reference the same constant.
pub const RESERVED_LISTENER_RECONNECTED: &str = "__alkstore_listener_reconnected__";

/// Which namespace class a name belongs to (ADR-008 §4).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NameKind {
    /// Shared-namespace kinds — channels, streams, queues, outboxes,
    /// locks, schedule names. Non-empty, and the reserved prefix is
    /// rejected.
    Shared,
    /// Consumer-local identifiers — stream-consumer names, `owner` on
    /// `try_lock`, `worker_id` on claims. Non-empty (whitespace-only
    /// rejected like any empty): a leading `__alkstore_` is legal
    /// (they are not a shared engine namespace, so there is nothing to
    /// collide with).
    Local,
}

/// Validate a name per its [`NameKind`].
///
/// - non-empty → [`Error::InvalidName`](crate::Error::InvalidName)
///   when empty; the emptiness check is the raw string's (`""`
///   rejects) with whitespace-only strings rejected by the same arm
///   (they are never a meaningful name on any engine — see below)
/// - `NameKind::Shared` additionally rejects names carrying
///   [`RESERVED_PREFIX`] with
///   [`Error::ReservedName`](crate::Error::ReservedName)
///
/// Whitespace-only names are rejected as
/// [`Error::InvalidName`](crate::Error::InvalidName) for
/// both kinds: they are never a meaningful name on any engine.
pub fn validate_name(kind: NameKind, name: &str) -> crate::Result<()> {
    if name.is_empty() || name.trim().is_empty() {
        return Err(crate::Error::InvalidName {
            name: name.to_string(),
        });
    }
    if kind == NameKind::Shared && name.starts_with(RESERVED_PREFIX) {
        return Err(crate::Error::ReservedName {
            name: name.to_string(),
        });
    }
    Ok(())
}

/// Validate a shared-namespace name (channels, streams, queues,
/// outboxes, locks, schedule names).
pub fn validate_shared_name(name: &str) -> crate::Result<()> {
    validate_name(NameKind::Shared, name)
}

/// Validate a consumer-local identifier (stream-consumer names, `owner`
/// on `try_lock`, `worker_id` on claims): non-empty only.
pub fn validate_local_name(name: &str) -> crate::Result<()> {
    validate_name(NameKind::Local, name)
}