soma-som-ring 0.1.0

Standalone ring execution engine for soma(som): cycle lifecycle, extension registration, boundary mediation
Documentation
// SPDX-License-Identifier: LGPL-3.0-only
#![allow(missing_docs)]

//! `RingProcessor`: the ring engine's minimal processing contract.
//!
//! `RingProcessor` is the trait every registered unit implementation
//! satisfies. The engine only knows that each unit has something that
//! transforms [`Quad`]s — concrete processor types live in the
//! application crate, not here.

use soma_som_core::quad::Quad;
use soma_som_core::types::{Layer, UnitId};

use crate::error::RingEngineError;

/// Minimal processing contract for ring units.
///
/// Implementations transform a stripped input Quad into an output Quad.
/// The engine calls `process()` for the vertical (Server→Server) path,
/// and `process_layer()` for the horizontal (Server→Client) fork.
///
/// ## Spec traceability
///
/// - Core §4.3: Server backbone with presentation fork
/// - Contracts §3: Vertical contract (Quad-in/Quad-out)
/// - Contracts §4: Horizontal contract (Server→Client boundary)
pub trait RingProcessor: Send {
    /// Process the vertical path (Server→Server ring backbone).
    ///
    /// `input` is the body-problem-stripped Quad received from the
    /// predecessor unit's Server layer. `data` is this unit's own
    /// Data layer — the resting memory accumulated across cycles.
    /// Processors that maintain internal state can read `data` to
    /// reconstruct after hot-swap.
    fn process(
        &mut self,
        unit: UnitId,
        cycle: u64,
        input: &Quad,
        data: &Quad,
    ) -> Result<Quad, RingEngineError>;

    /// Process the horizontal path (Server→Client presentation fork).
    ///
    /// Default: clone-through (passthrough). Override when a unit needs
    /// to transform the presentation fork separately from the vertical
    /// ring backbone.
    fn process_layer(
        &mut self,
        _unit: UnitId,
        _cycle: u64,
        _layer: Layer,
        input: &Quad,
    ) -> Result<Quad, RingEngineError> {
        Ok(input.clone())
    }

    /// Externalize accumulated processor state for Data layer deposit.
    ///
    /// Called after `process()` each cycle. If `Some(quad)`, the engine
    /// deposits the Quad into this unit's Data layer, making the state
    /// ring-observable, persistable, and recoverable after hot-swap.
    ///
    /// Default: `None` — no state to externalize (backward compatible).
    fn externalize_state(&self, _unit: UnitId) -> Option<Quad> {
        None
    }
}

// inline: exercises module-private items via super::*
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ring_processor_is_object_safe() {
        fn _assert(_: &dyn RingProcessor) {}
    }

    /// Minimal test processor: echoes input as output.
    struct EchoProcessor;

    impl RingProcessor for EchoProcessor {
        fn process(
            &mut self,
            _unit: UnitId,
            _cycle: u64,
            input: &Quad,
            _data: &Quad,
        ) -> Result<Quad, RingEngineError> {
            Ok(input.clone())
        }
    }

    #[test]
    fn echo_processor_vertical() {
        let mut proc = EchoProcessor;
        let input = Quad::default();
        let output = proc
            .process(UnitId::FU, 1, &input, &Quad::default())
            .unwrap();
        assert_eq!(output, input);
    }

    #[test]
    fn default_process_layer_is_passthrough() {
        let mut proc = EchoProcessor;
        let input = Quad::default();
        let output = proc
            .process_layer(UnitId::FU, 1, Layer::Client, &input)
            .unwrap();
        assert_eq!(output, input);
    }

    #[test]
    fn boxed_processor_works() {
        let proc: Box<dyn RingProcessor> = Box::new(EchoProcessor);
        // Verify we can hold it as a trait object
        assert_eq!(
            std::mem::size_of_val(&proc),
            std::mem::size_of::<Box<dyn RingProcessor>>()
        );
    }
}