statum-core 0.9.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
//! - optional machine introspection and presentation descriptors generated by
//!   `#[machine]` / `#[transition]` when the `introspection` feature is enabled

use std::borrow::Cow;

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

#[cfg(feature = "introspection")]
mod introspection;

pub mod projection;
#[cfg(feature = "introspection")]
pub mod testing;

#[doc(hidden)]
pub mod __private {
    #[cfg(feature = "introspection")]
    pub use crate::{
        MachinePresentation, MachinePresentationDescriptor, RebuildAmbiguity, RebuildAttempt,
        RebuildInput, RebuildReport, StatePresentation, TransitionPresentation,
        TransitionPresentationInventory,
    };
    #[cfg(feature = "introspection")]
    pub use linkme;

    #[cfg(feature = "introspection")]
    #[derive(Debug)]
    pub struct TransitionToken {
        _private: u8,
    }

    #[cfg(feature = "introspection")]
    impl Default for TransitionToken {
        fn default() -> Self {
            Self::new()
        }
    }

    #[cfg(feature = "introspection")]
    impl TransitionToken {
        pub const fn new() -> Self {
            Self { _private: 0 }
        }
    }
}

#[cfg(feature = "introspection")]
pub use introspection::{
    GraphAuthorityLevel, GraphLintCode, GraphLintFinding, MachineDescriptor, MachineGraph,
    MachineIntrospection, MachinePresentation, MachinePresentationDescriptor, MachineStateIdentity,
    MachineTransitionRecorder, RecordedTransition, StableFieldMetadata, StableGraphMetadata,
    StableGraphMetadataVersion, StableMachineMetadata, StableStateMetadata,
    StableTransitionMetadata, StateDescriptor, StatePresentation, TransitionDescriptor,
    TransitionInventory, TransitionPresentation, TransitionPresentationInventory,
    TransitionTelemetryLabels, UnsupportedGraphMetadataCase,
};

/// 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 {}

/// 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),
}

/// 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 accepted the input. In ambiguity-checking reports,
    /// multiple accepted validators can still leave the final rebuild result invalid.
    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>>,
}

/// Describes the persisted input that a rebuild report evaluated.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RebuildInput {
    /// Rust type name of the persisted input shape.
    pub type_name: &'static str,
    /// Optional stable identifier for the specific persisted input.
    pub identifier: Option<Cow<'static, str>>,
}

/// Ambiguity status for a rebuild report.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RebuildAmbiguity {
    /// The generated rebuild surface stopped at the first match and did not scan
    /// for additional matching validators.
    NotChecked,
    /// All candidates were evaluated and at most one matched.
    Unambiguous,
    /// Multiple candidates matched the same persisted input.
    Ambiguous {
        /// State candidates whose validators accepted the input.
        matched_states: Vec<&'static str>,
    },
}

/// A typed rehydration result plus the validator attempts that produced it.
#[derive(Debug)]
#[non_exhaustive]
pub struct RebuildReport<M> {
    /// Rust machine type whose validators were evaluated.
    pub machine: &'static str,
    /// Persisted input shape and optional stable input identifier.
    pub input: RebuildInput,
    /// State candidates considered by the generated rebuild surface.
    pub candidate_states: Vec<&'static str>,
    /// Whether this report checked for multiple matching validators.
    pub ambiguity: RebuildAmbiguity,
    /// Validator attempts in evaluation order.
    pub attempts: Vec<RebuildAttempt>,
    /// Final rebuild result.
    pub result: Result<M>,
}

impl<M> RebuildReport<M> {
    /// Create a structured rebuild report.
    pub fn new(
        machine: &'static str,
        input: RebuildInput,
        candidate_states: Vec<&'static str>,
        ambiguity: RebuildAmbiguity,
        attempts: Vec<RebuildAttempt>,
        result: Result<M>,
    ) -> Self {
        Self {
            machine,
            input,
            candidate_states,
            ambiguity,
            attempts,
            result,
        }
    }

    /// Attach a stable persisted-input identifier for logs or admin UIs.
    pub fn with_input_identifier(mut self, identifier: impl Into<Cow<'static, str>>) -> Self {
        self.input.identifier = Some(identifier.into());
        self
    }

    /// 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 {}