cotis-modules-debug 0.1.0-alpha

Record, replay, and isolate Cotis layout and UI declaration pipelines
Documentation
//! Serializable UI declaration instruction streams and leaf-config type lists.
//!
//! This module records the sequence of operations performed while building a Cotis UI
//! ([`Open`](SerializableInterfaceInstructions::Open), [`Configure`](SerializableInterfaceInstructions::Configure),
//! [`Close`](SerializableInterfaceInstructions::Close), etc.) as JSON-friendly data. At replay
//! time, the stream is interpreted to rebuild the element tree without the original UI code.
//!
//! Leaf element configs (text, images, etc.) are stored in a type-erased [`LeafConfigData`]
//! blob. The [`LeafConfigList`](leaf_serialization::LeafConfigList) type parameter on
//! [`LeafedInterfaceDeclaration`] tells the replay machinery which leaf types to try when
//! deserializing those blobs.
//!
//! # Instruction protocol
//!
//! A valid recording for one element subtree looks like:
//!
//! ```text
//! Open → Configure(config)? → LeafConfig(leaf)? → (child subtrees)* → Close
//! ```
//!
//! [`Start`](SerializableInterfaceInstructions::Start) and
//! [`End`](SerializableInterfaceInstructions::End) are optional session markers written around
//! a frame; they are persisted to JSON but **ignored during replay**.
//!
//! # Stage 2 replay
//!
//! [`LeafedInterfaceDeclaration`] implements [`UiFn`] for shared
//! references, so a loaded declaration can be passed directly to
//! `CotisApp::compute_frame` as the UI callback.

use std::path::Path;

use cotis::cotis_app::UiFn;
use cotis::element_configuring::{ConfigureElements, ConfiguredParentElement};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};

use crate::layout_module_debugger::isolated_replay::replay_instructions;
use crate::serialize_targets::leaf_serialization::{LeafConfigData, LeafConfigListCompatible};

pub mod leaf_serialization;

/// One step in a recorded UI declaration.
///
/// Together, a sequence of these instructions describes how an element tree was built.
/// Only [`Open`](Self::Open), [`Close`](Self::Close), [`Configure`](Self::Configure), and
/// [`LeafConfig`](Self::LeafConfig) affect replay. [`Start`](Self::Start) and
/// [`End`](Self::End) are optional frame markers that are ignored on replay.
#[derive(Serialize, Deserialize, Debug)]
pub enum SerializableInterfaceInstructions<ConfigType> {
    /// Optional session/frame start marker (ignored on replay).
    Start,
    /// Opens a new element in the tree.
    Open,
    /// Sets the parent/element config for the current open element.
    Configure(ConfigType),
    /// Sets a leaf-specific config (type-erased JSON blob).
    LeafConfig(LeafConfigData),
    /// Closes the current open element.
    Close,
    /// Optional session/frame end marker (ignored on replay).
    End,
}

/// Append-only builder for a serialized UI declaration instruction stream.
///
/// Use the builder methods to record operations in the same order they occur during live UI
/// construction. The resulting stream can be serialized to JSON via
/// [`LeafedInterfaceDeclaration::store_in`].
///
/// # Examples
///
/// ```rust,ignore
/// let mut decl = SerializedInterfaceDeclaration::<MyConfig>::new();
/// decl.start();
/// decl.open();
/// decl.add_config(my_config);
/// decl.close();
/// decl.end();
/// ```
#[derive(Serialize, Deserialize, Debug)]
pub struct SerializedInterfaceDeclaration<ConfigType> {
    configs: Vec<SerializableInterfaceInstructions<ConfigType>>,
}

impl<Config> SerializedInterfaceDeclaration<Config> {
    /// Creates an empty declaration with no recorded instructions.
    pub fn new() -> Self {
        Self { configs: vec![] }
    }

    /// Records a [`Start`](SerializableInterfaceInstructions::Start) marker.
    pub fn start(&mut self) {
        self.configs.push(SerializableInterfaceInstructions::Start);
    }

    /// Records an [`End`](SerializableInterfaceInstructions::End) marker.
    pub fn end(&mut self) {
        self.configs.push(SerializableInterfaceInstructions::End);
    }

    /// Records an [`Open`](SerializableInterfaceInstructions::Open) instruction.
    pub fn open(&mut self) {
        self.configs.push(SerializableInterfaceInstructions::Open);
    }

    /// Records a [`Configure`](SerializableInterfaceInstructions::Configure) instruction.
    pub fn add_config(&mut self, config: Config) {
        self.configs
            .push(SerializableInterfaceInstructions::Configure(config));
    }

    /// Records a [`Close`](SerializableInterfaceInstructions::Close) instruction.
    pub fn close(&mut self) {
        self.configs.push(SerializableInterfaceInstructions::Close);
    }

    /// Records a [`LeafConfig`](SerializableInterfaceInstructions::LeafConfig) instruction.
    pub fn add_leaf_config<LeafConfig: Serialize>(&mut self, config: &LeafConfig) {
        self.configs
            .push(SerializableInterfaceInstructions::LeafConfig(
                LeafConfigData::new(config),
            ));
    }

    /// Iterates over the recorded instructions in order.
    pub fn iter_over(&self) -> impl Iterator<Item = &SerializableInterfaceInstructions<Config>> {
        self.configs.iter()
    }
}

impl<Config> Default for SerializedInterfaceDeclaration<Config> {
    fn default() -> Self {
        Self::new()
    }
}

/// A serialized UI declaration paired with a leaf-config type list for replay.
///
/// The `List` type parameter is a phantom [`LeafConfigList`](leaf_serialization::LeafConfigList)
/// chain that determines which leaf config types are tried when deserializing
/// [`LeafConfig`](SerializableInterfaceInstructions::LeafConfig) instructions. It is not
/// serialized to disk; callers must supply the same list at load time.
pub struct LeafedInterfaceDeclaration<ConfigType, List: Copy> {
    serialized: SerializedInterfaceDeclaration<ConfigType>,
    leaf_types: List,
}

/// One instruction paired with the leaf type list used for replay dispatch.
pub struct LeafedInterfaceInstructions<ConfigType, List: Copy> {
    /// The instruction being replayed.
    pub instruction: ConfigType,
    /// Copy of the leaf type list carried through the iterator.
    pub list: List,
}

impl<ConfigType, List: Copy> LeafedInterfaceDeclaration<ConfigType, List> {
    /// Creates a declaration from a serialized stream and a leaf type list.
    pub fn new(serialized: SerializedInterfaceDeclaration<ConfigType>, leaf_types: List) -> Self {
        Self {
            serialized,
            leaf_types,
        }
    }

    /// Records a [`Start`](SerializableInterfaceInstructions::Start) marker.
    pub fn start(&mut self) {
        self.serialized.start();
    }

    /// Records an [`End`](SerializableInterfaceInstructions::End) marker.
    pub fn end(&mut self) {
        self.serialized.end();
    }

    /// Records an [`Open`](SerializableInterfaceInstructions::Open) instruction.
    pub fn open(&mut self) {
        self.serialized.open();
    }

    /// Records a [`Configure`](SerializableInterfaceInstructions::Configure) instruction.
    pub fn add_config(&mut self, config: ConfigType) {
        self.serialized.add_config(config);
    }

    /// Records a [`Close`](SerializableInterfaceInstructions::Close) instruction.
    pub fn close(&mut self) {
        self.serialized.close();
    }

    /// Records a [`LeafConfig`](SerializableInterfaceInstructions::LeafConfig) instruction.
    pub fn add_leaf_config<LeafConfig: Serialize>(&mut self, config: &LeafConfig) {
        self.serialized.add_leaf_config(config);
    }

    /// Iterates instructions paired with the leaf type list for replay.
    pub fn iter_over(
        &self,
    ) -> impl Iterator<
        Item = LeafedInterfaceInstructions<&SerializableInterfaceInstructions<ConfigType>, List>,
    > {
        let list = self.leaf_types;
        self.serialized
            .iter_over()
            .map(move |instruction| LeafedInterfaceInstructions { instruction, list })
    }
}

impl<ConfigType: Serialize + DeserializeOwned, List: Copy>
    LeafedInterfaceDeclaration<ConfigType, List>
{
    /// Serializes the instruction stream to a JSON file.
    ///
    /// Only the instruction vector is written; the leaf type list is not persisted.
    ///
    /// # Errors
    ///
    /// Returns an error if JSON serialization or file writing fails.
    pub fn store_in(&self, file: &Path) -> Result<(), Box<dyn std::error::Error>> {
        let bytes = serde_json::to_vec(&self.serialized)?;
        std::fs::write(file, bytes)?;
        Ok(())
    }

    /// Loads an instruction stream from a JSON file.
    ///
    /// The caller must supply the same [`LeafConfigList`](leaf_serialization::LeafConfigList)
    /// type chain that was used during recording.
    ///
    /// # Errors
    ///
    /// Returns an error if file reading or JSON deserialization fails.
    pub fn load_from(file: &Path, leaf_types: List) -> Result<Self, Box<dyn std::error::Error>> {
        let bytes = std::fs::read(file)?;
        let serialized: SerializedInterfaceDeclaration<ConfigType> =
            serde_json::from_slice(&bytes)?;
        Ok(Self {
            serialized,
            leaf_types,
        })
    }
}

impl<
    'inside,
    Configurer: ConfigureElements<ConfigType>,
    ConfigType: Clone,
    List: Copy + LeafConfigListCompatible<Configurer, ConfigType>,
> UiFn<'inside, Configurer, ConfigType> for &LeafedInterfaceDeclaration<ConfigType, List>
{
    /// Replays the stored declaration into the layout root (stage 2 of the debug pipeline).
    ///
    /// Interprets the instruction stream to rebuild the element tree without debug wrappers.
    ///
    /// # Panics
    ///
    /// Panics if the instruction stream is invalid:
    ///
    /// - `"Bad replay: cannot configure ConfiguredParentElement"` — `Configure` at parent scope.
    /// - `"Bad replay: cannot configure ConfiguredParentElement with leaf config"` — `LeafConfig`
    ///   at parent scope.
    fn draw_in(self, layout: &mut ConfiguredParentElement<'inside, Configurer, ConfigType>) {
        replay_instructions(&mut self.iter_over(), layout);
    }
}