liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
use super::{
    AtomicU64, CONTRACT_UNSUPPORTED, CONTRACT_VERSION, CURSOR_FOREIGN, HotAction, LOG_MISMATCH,
    Mode, Ordering, ServerError,
};

/// The two membership actions. Read and post are separate: an answer about
/// one says nothing of the other (ACCESS-006 R1).
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum MembershipAction {
    /// Reading a channel: a subscription, a delivered reply, result or
    /// request.
    Read,
    /// Posting on a channel: a publish or an outbound service effect.
    Post,
}

impl MembershipAction {
    /// The contract's action word.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Read => "read",
            Self::Post => "post",
        }
    }

    /// The membership action a hot route needs: a publish and a call post,
    /// a subscription and a served request read. A call or serve still needs
    /// its own operation grant beside the membership; membership never
    /// implies it.
    #[must_use]
    pub const fn for_hot(action: HotAction) -> Self {
        match action {
            HotAction::Publish | HotAction::Call => Self::Post,
            HotAction::Subscribe | HotAction::Serve => Self::Read,
        }
    }
}

/// Why a protected act, delivery or page was refused.
///
/// Every variant has a
/// stable [`Self::name`]; none carries a payload, a credential, a resource
/// id or a channel name, so a refusal can cross any boundary whole.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MembershipRefusal {
    /// The connection proved no Lys holder, so it has no membership.
    HolderUnverified,
    /// The admitted pass did not name its holder's directory kind.
    HolderKindUnknown,
    /// The verified holder's id or kind is empty or carries control
    /// characters.
    HolderMalformed,
    /// No ACCESS-005 grant authority is installed, so no membership is
    /// current.
    AuthorityMissing,
    /// The grant authority has not established its verified baseline.
    NotReady,
    /// The grant authority or the provider could not be asked, by cause.
    Unavailable {
        /// The stable cause, naming no response body.
        cause: String,
    },
    /// The grant authority's change feed has an uncovered interval.
    Gap {
        /// The last revision applied before the gap.
        after: u64,
        /// How many revisions are missing.
        missing: u64,
    },
    /// The grant log was reset; its revisions no longer compare.
    Reset,
    /// The authority moved to a generation this index has not yet resumed.
    ResumePending,
    /// A change or a resume is re-deciding this binding.
    ChangeApplying,
    /// The answer or the authority names another grant log or epoch.
    LogMismatch,
    /// The provider answered under a contract this liminal does not speak.
    ContractUnsupported {
        /// The contract version the provider named.
        contract: u32,
    },
    /// The answer echoes a request other than the one asked: a substituted
    /// subject, resource, workspace or action.
    AnswerSubstituted,
    /// The answer reflects an older revision than the one required.
    RevisionStale {
        /// The least revision required.
        required: u64,
        /// The revision decided at, if any.
        decided: Option<u64>,
    },
    /// The allowing scope is neither the mapped channel nor its workspace.
    ScopeOutsideMapping,
    /// Lys refused the membership by its stable name.
    Refused {
        /// The provider's stable refusal name.
        refusal: String,
    },
    /// A grant reaches the act, but only by approval.
    Held {
        /// The approval the grant requires.
        mode: Mode,
    },
    /// The index is at its configured bound; nothing was allocated.
    CapacityExceeded {
        /// The configured bound.
        limit: usize,
    },
    /// The index lock was poisoned by a panic elsewhere.
    IndexPoisoned,
    /// The workspace asked about is not in the product mapping.
    WorkspaceNotMapped,
    /// The channel asked about is not in the product mapping.
    ChannelNotMapped,
    /// The continuation was issued for another question.
    CursorForeign,
    /// The continuation was issued at another generation or log epoch.
    CursorStale,
    /// A page exceeded its row or byte bound.
    PageOverBound {
        /// The rows the page carried.
        rows: u64,
        /// The encoded bytes the rows carried.
        bytes: u64,
    },
    /// A page row answers another subject, kind, resource or workspace.
    PageForeignRow,
    /// A page listed one row twice.
    PageDuplicateRow,
    /// A page's counts, completion and continuation disagree.
    PageInconsistent,
}

impl MembershipRefusal {
    /// Whether this refusal holds rather than denies (ACCESS-006 R2): the
    /// authority or the index is not current, so a queued protected delivery
    /// waits, releasing nothing, rather than being retired. Every other
    /// refusal is a decided deny.
    #[must_use]
    pub const fn holds(&self) -> bool {
        matches!(
            self,
            Self::AuthorityMissing
                | Self::NotReady
                | Self::Unavailable { .. }
                | Self::Gap { .. }
                | Self::Reset
                | Self::ResumePending
                | Self::ChangeApplying
                | Self::IndexPoisoned
        )
    }

    /// The stable machine name.
    #[must_use]
    pub fn name(&self) -> &str {
        match self {
            Self::HolderUnverified => "membership_holder_unverified",
            Self::HolderKindUnknown => "membership_holder_kind_unknown",
            Self::HolderMalformed => "membership_holder_malformed",
            Self::AuthorityMissing => "membership_authority_missing",
            Self::NotReady => "membership_not_ready",
            Self::Unavailable { .. } => "membership_unavailable",
            Self::Gap { .. } => "membership_gap",
            Self::Reset => "membership_log_reset",
            Self::ResumePending => "membership_resume_pending",
            Self::ChangeApplying => "membership_change_applying",
            Self::LogMismatch => LOG_MISMATCH,
            Self::ContractUnsupported { .. } => CONTRACT_UNSUPPORTED,
            Self::AnswerSubstituted => "membership_answer_substituted",
            Self::RevisionStale { .. } => "membership_revision_stale",
            Self::ScopeOutsideMapping => "membership_scope_outside_mapping",
            Self::Refused { refusal } => refusal,
            Self::Held { .. } => "membership_held",
            Self::CapacityExceeded { .. } => "membership_capacity_exceeded",
            Self::IndexPoisoned => "membership_index_poisoned",
            Self::WorkspaceNotMapped => "membership_workspace_not_mapped",
            Self::ChannelNotMapped => "membership_channel_not_mapped",
            Self::CursorForeign => CURSOR_FOREIGN,
            Self::CursorStale => "membership_cursor_stale",
            Self::PageOverBound { .. } => "membership_page_over_bound",
            Self::PageForeignRow => "membership_page_foreign_row",
            Self::PageDuplicateRow => "membership_page_duplicate_row",
            Self::PageInconsistent => "membership_page_inconsistent",
        }
    }
}

impl std::fmt::Display for MembershipRefusal {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Unavailable { cause } => write!(formatter, "{}: {cause}", self.name()),
            Self::Gap { after, missing } => write!(
                formatter,
                "{}: {missing} revisions missing after {after}",
                self.name()
            ),
            Self::ContractUnsupported { contract } => write!(
                formatter,
                "{}: contract {contract}; this liminal speaks {CONTRACT_VERSION}",
                self.name()
            ),
            Self::RevisionStale { required, decided } => write!(
                formatter,
                "{}: required {required}, decided {decided:?}",
                self.name()
            ),
            Self::Held { mode } => write!(formatter, "{}: {mode:?}", self.name()),
            Self::CapacityExceeded { limit } => {
                write!(formatter, "{}: limit {limit}", self.name())
            }
            Self::PageOverBound { rows, bytes } => {
                write!(formatter, "{}: {rows} rows, {bytes} bytes", self.name())
            }
            _ => formatter.write_str(self.name()),
        }
    }
}

impl std::error::Error for MembershipRefusal {}

pub(super) fn malformed(value: &str) -> bool {
    value.is_empty() || value.chars().any(char::is_control)
}

pub(super) fn invalid(message: impl Into<String>) -> ServerError {
    ServerError::ConfigValidation {
        message: message.into(),
    }
}

pub(super) fn bump(counter: &AtomicU64) {
    counter.fetch_add(1, Ordering::Relaxed);
}

pub(super) fn bump_by(counter: &AtomicU64, count: u64) {
    counter.fetch_add(count, Ordering::Relaxed);
}