statum-core 0.7.0

Core types for representing legal workflow and protocol states explicitly in Rust
Documentation
//! Core traits and helper types shared by Statum crates.
//!
//! Most users reach these through the top-level `statum` crate. This crate
//! holds the small runtime surface that macro-generated code targets:
//!
//! - state marker traits
//! - transition capability traits
//! - runtime error and result types
//! - projection helpers for event-log style rebuilds

use std::borrow::Cow;

#[cfg(doctest)]
#[doc = include_str!("../README.md")]
mod readme_doctests {}

mod introspection;

pub mod projection;

#[doc(hidden)]
pub use introspection::__STATUM_LINKED_MACHINES;
#[doc(hidden)]
pub use introspection::__STATUM_LINKED_REFERENCE_TYPES;
#[doc(hidden)]
pub use introspection::__STATUM_LINKED_RELATIONS;
#[doc(hidden)]
pub use introspection::__STATUM_LINKED_VALIDATOR_ENTRIES;
#[doc(hidden)]
pub use introspection::__STATUM_LINKED_VIA_ROUTES;

#[doc(hidden)]
pub mod __private {
    pub use crate::{
        Attested, LinkedMachineGraph, LinkedReferenceTypeDescriptor, LinkedRelationBasis, LinkedRelationDescriptor,
        LinkedRelationKind, LinkedRelationSource, LinkedRelationTarget, LinkedStateDescriptor,
        LinkedTransitionDescriptor, LinkedTransitionInventory, LinkedValidatorEntryDescriptor,
        LinkedViaRouteDescriptor, MachinePresentation,
        MachinePresentationDescriptor, MachineReference, MachineReferenceTarget, MachineRole,
        RebuildAttempt, RebuildReport, StateFamily, StateFamilyMember, StatePresentation,
        StaticMachineLinkDescriptor, TransitionPresentation, TransitionPresentationInventory,
        __STATUM_LINKED_MACHINES, __STATUM_LINKED_REFERENCE_TYPES, __STATUM_LINKED_RELATIONS,
        __STATUM_LINKED_VALIDATOR_ENTRIES, __STATUM_LINKED_VIA_ROUTES,
    };
    pub use futures;
    pub use linkme;

    #[derive(Debug)]
    pub struct TransitionToken {
        _private: u8,
    }

    impl Default for TransitionToken {
        fn default() -> Self {
            Self::new()
        }
    }

    impl TransitionToken {
        pub const fn new() -> Self {
            Self { _private: 0 }
        }
    }

    pub fn attest<T, Via>(inner: T) -> crate::Attested<T, Via> {
        crate::Attested::new(inner)
    }
}

pub use introspection::{
    linked_machines, linked_reference_types, linked_relations, linked_validator_entries,
    linked_via_routes, LinkedMachineGraph, LinkedReferenceTypeDescriptor, LinkedRelationBasis,
    LinkedRelationDescriptor, LinkedRelationKind, LinkedRelationSource, LinkedRelationTarget,
    LinkedStateDescriptor, LinkedTransitionDescriptor, LinkedTransitionInventory,
    LinkedValidatorEntryDescriptor, LinkedViaRouteDescriptor, MachineDescriptor, MachineGraph,
    MachineIntrospection, MachinePresentation, MachineRole, MachinePresentationDescriptor,
    MachineStateIdentity, MachineTransitionRecorder, RecordedTransition, StateDescriptor,
    StatePresentation, StaticMachineLinkDescriptor, TransitionDescriptor, TransitionInventory,
    TransitionPresentation, TransitionPresentationInventory,
};

/// Hidden family-level metadata emitted by `#[state]`.
#[doc(hidden)]
pub trait StateFamily {
    /// Rust-facing enum name for this family.
    const NAME: &'static str;

    /// Number of generated legal state markers in this family.
    const VARIANT_COUNT: usize;
}

/// Hidden per-marker metadata emitted by `#[state]`.
#[doc(hidden)]
pub trait StateFamilyMember: StateMarker {
    /// Rust-facing marker name for this state.
    const RUST_NAME: &'static str;

    /// Whether this state carries data.
    const HAS_DATA: bool;
}

/// A generated state marker type.
///
/// Every `#[state]` variant produces one marker type that implements
/// `StateMarker`. The associated `Data` type is `()` for unit states and the
/// tuple payload type for data-bearing states.
pub trait StateMarker {
    /// The payload type stored in machines for this state.
    type Data;
}

/// A generated state marker with no payload.
///
/// Implemented for unit state variants like `Draft` or `Published`.
pub trait UnitState: StateMarker<Data = ()> {}

/// A generated state marker that carries payload data.
///
/// Implemented for tuple variants like `InReview(Assignment)`.
pub trait DataState: StateMarker {}

/// One exact target declared for a nominal machine reference type.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MachineReferenceTarget {
    /// Exact machine path segments resolved from the declaration target.
    pub machine_path: &'static [&'static str],
    /// Target state marker name written in the declaration.
    pub state: &'static str,
}

/// One nominal type that carries an exact machine relation declared once.
pub trait MachineReference {
    /// Exact target described by this nominal reference type.
    const TARGET: MachineReferenceTarget;
}

/// A machine that can transition directly to `Next`.
///
/// This is the stable trait-level view of `self.transition()`.
pub trait CanTransitionTo<Next> {
    /// The transition result type.
    type Output;

    /// Perform the transition.
    fn transition_to(self) -> Self::Output;
}

/// A machine that can transition using `Data`.
///
/// This is the stable trait-level view of `self.transition_with(data)`.
pub trait CanTransitionWith<Data> {
    /// The next state selected by this transition.
    type NextState;
    /// The transition result type.
    type Output;

    /// Perform the transition with payload data.
    fn transition_with_data(self, data: Data) -> Self::Output;
}

/// A machine that can transition by mapping its current state data into `Next`.
///
/// This is the stable trait-level view of `self.transition_map(...)`.
pub trait CanTransitionMap<Next: StateMarker> {
    /// The payload type stored in the current state.
    type CurrentData;
    /// The transition result type.
    type Output;

    /// Perform the transition by consuming the current state data and producing the next payload.
    fn transition_map<F>(self, f: F) -> Self::Output
    where
        F: FnOnce(Self::CurrentData) -> Next::Data;
}

/// Errors returned by Statum runtime helpers.
#[derive(Debug)]
pub enum Error {
    /// Returned when a runtime check determines the current state is invalid.
    InvalidState,
}

/// A first-class two-way branching transition result.
///
/// This lets a transition expose two concrete machine targets while keeping the
/// branch alternatives visible to Statum introspection.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Branch<A, B> {
    /// The first legal target branch.
    First(A),
    /// The second legal target branch.
    Second(B),
}

/// A machine value with attached typed provenance for how it was produced.
///
/// Statum uses this wrapper for attested transition routes such as
/// `capture_and_attest()` and generated `from_*()` binders. The public surface
/// exposes the wrapped machine and the route type, but construction stays on
/// the macro-generated path so ordinary callers cannot forge attestation
/// accidentally.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Attested<T, Via> {
    inner: T,
    marker: core::marker::PhantomData<Via>,
}

impl<T, Via> Attested<T, Via> {
    #[doc(hidden)]
    pub fn new(inner: T) -> Self {
        Self {
            inner,
            marker: core::marker::PhantomData,
        }
    }

    /// Consumes the wrapper and returns the attested inner value.
    pub fn into_inner(self) -> T {
        self.inner
    }

    /// Borrows the attested inner value.
    #[allow(clippy::should_implement_trait)]
    pub fn as_ref(&self) -> &T {
        &self.inner
    }

    /// Maps the inner value while preserving the attested route marker.
    pub fn map_inner<U>(self, f: impl FnOnce(T) -> U) -> Attested<U, Via> {
        Attested::new(f(self.inner))
    }
}

impl<T, Via> AsRef<T> for Attested<T, Via> {
    fn as_ref(&self) -> &T {
        &self.inner
    }
}

/// Convenience result alias used by Statum APIs.
///
/// # Example
///
/// ```
/// fn ensure_ready(ready: bool) -> statum_core::Result<()> {
///     if ready {
///         Ok(())
///     } else {
///         Err(statum_core::Error::InvalidState)
///     }
/// }
///
/// assert!(ensure_ready(true).is_ok());
/// assert!(ensure_ready(false).is_err());
/// ```
pub type Result<T> = core::result::Result<T, Error>;

/// A structured validator rejection captured during typed rehydration.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Rejection {
    /// Stable machine-readable reason key for why the validator rejected.
    pub reason_key: &'static str,
    /// Optional human-readable message for debugging and reports.
    pub message: Option<Cow<'static, str>>,
}

impl Rejection {
    /// Create a rejection with a stable reason key and no message.
    pub const fn new(reason_key: &'static str) -> Self {
        Self {
            reason_key,
            message: None,
        }
    }

    /// Attach a human-readable message to this rejection.
    pub fn with_message(self, message: impl Into<Cow<'static, str>>) -> Self {
        Self {
            message: Some(message.into()),
            ..self
        }
    }
}

impl From<&'static str> for Rejection {
    fn from(reason_key: &'static str) -> Self {
        Self::new(reason_key)
    }
}

impl core::fmt::Display for Rejection {
    fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match &self.message {
            Some(message) => write!(fmt, "{}: {}", self.reason_key, message),
            None => write!(fmt, "{}", self.reason_key),
        }
    }
}

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

/// An opt-in validator result that carries structured rejection details.
pub type Validation<T> = core::result::Result<T, Rejection>;

/// One validator evaluation recorded during typed rehydration.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RebuildAttempt {
    /// Rust method name of the validator that ran.
    pub validator: &'static str,
    /// Rust state-marker name the validator was checking.
    pub target_state: &'static str,
    /// Whether this validator matched and produced the rebuilt state.
    pub matched: bool,
    /// Stable machine-readable rejection key, when the validator exposed one.
    pub reason_key: Option<&'static str>,
    /// Optional human-readable rejection message, when the validator exposed one.
    pub message: Option<Cow<'static, str>>,
}

/// A typed rehydration result plus the validator attempts that produced it.
#[derive(Debug)]
pub struct RebuildReport<M> {
    /// Validator attempts in evaluation order.
    pub attempts: Vec<RebuildAttempt>,
    /// Final rebuild result.
    pub result: Result<M>,
}

impl<M> RebuildReport<M> {
    /// Returns the first matching validator attempt, if any.
    pub fn matched_attempt(&self) -> Option<&RebuildAttempt> {
        self.attempts.iter().find(|attempt| attempt.matched)
    }

    /// Consumes the report and returns the original rebuild result.
    pub fn into_result(self) -> Result<M> {
        self.result
    }
}

impl<T> From<Error> for core::result::Result<T, Error> {
    fn from(val: Error) -> Self {
        Err(val)
    }
}

impl core::fmt::Display for Error {
    fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::result::Result<(), core::fmt::Error> {
        write!(fmt, "{self:?}")
    }
}

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