cotis-modules-debug 0.1.0-alpha

Record, replay, and isolate Cotis layout and UI declaration pipelines
Documentation
//! Type-level lists for deserializing type-erased leaf configs at replay time.
//!
//! During recording, leaf element configs (text, images, custom widgets) are serialized into
//! [`LeafConfigData`] blobs without preserving their Rust type. At replay, the
//! [`LeafConfigList`] chain tells the interpreter which concrete types to try, in order.
//!
//! # Building a leaf type list
//!
//! Start with [`LeafConfigListNil`] (the empty list) and extend it using [`LeafConfigList::new`]
//! or [`LeafConfigList::extend`]:
//!
//! ```rust,ignore
//! use cotis_modules_debug::serialize_targets::leaf_serialization::{LeafConfigList, LeafConfigListNil};
//!
//! // Single leaf type:
//! type Leafs = LeafConfigList<TextElementConfig<'static>, LeafConfigListNil>;
//! let leafs = Leafs::new();
//!
//! // Multiple leaf types (order matters — see below):
//! type Leafs = LeafConfigList<
//!     TextElementConfig<'static>,
//!     LeafConfigList<ImageElementConfig<'static>, LeafConfigListNil>,
//! >;
//! let leafs = LeafConfigList::<TextElementConfig<'static>, _>::extend();
//! ```
//!
//! # Deserialization order
//!
//! When replaying a [`LeafConfig`](super::SerializableInterfaceInstructions::LeafConfig)
//! instruction, the list tries to deserialize the JSON blob as the head type first. If that
//! fails, it recurses to the tail. **Put more specific leaf types before more general ones**
//! to avoid ambiguous matches.

use std::marker::PhantomData;

use cotis::element_configuring::{
    ConfigureElements, ConfigureLeafElements, OpenElement, OpenLeafElement,
};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};

/// Terminal node of a [`LeafConfigList`] chain (empty list).
#[derive(Clone, Copy, Debug)]
pub struct LeafConfigListNil;

/// A type-level cons-list of leaf config types used for replay deserialization.
///
/// This is a phantom type: it carries no runtime data. Use [`new`](Self::new) to start a
/// one-element list, [`extend`](Self::extend) to prepend another type, or
/// [`new_long`](Self::new_long) when the full `Head`/`Tail` types are already known.
#[derive(Debug)]
pub struct LeafConfigList<Head, Tail>(PhantomData<(Head, Tail)>);

impl<Head, Tail> Clone for LeafConfigList<Head, Tail> {
    fn clone(&self) -> Self {
        *self
    }
}

impl<Head, Tail> Copy for LeafConfigList<Head, Tail> {}

impl<Head, Tail> LeafConfigList<Head, Tail> {
    /// Creates a one-element list with `Head` as the only leaf type.
    pub fn new() -> LeafConfigList<Head, LeafConfigListNil> {
        LeafConfigList(PhantomData)
    }

    /// Prepends `Head` to an existing `Head2`/`Tail2` list.
    pub fn extend<Head2, Tail2>() -> LeafConfigList<Head, LeafConfigList<Head2, Tail2>> {
        LeafConfigList(PhantomData)
    }

    /// Creates a list with the `Head` and `Tail` types already specified in the struct.
    pub fn new_long() -> LeafConfigList<Head, Tail> {
        LeafConfigList(PhantomData)
    }
}

/// Type-erased JSON storage for a leaf element config recorded during stage 1.
///
/// The concrete type is recovered at replay time using a [`LeafConfigList`] chain.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LeafConfigData {
    internal: Vec<u8>,
}

impl LeafConfigData {
    pub(crate) fn new<Head: Serialize>(config: &Head) -> LeafConfigData {
        Self {
            internal: serde_json::to_vec(&config).unwrap(),
        }
    }

    fn try_reconstruct<Head: DeserializeOwned>(&self) -> Option<Head> {
        serde_json::from_slice(&self.internal).ok()
    }
}

/// Type-level dispatch for replaying [`LeafConfigData`] against open elements.
///
/// Implemented automatically for [`LeafConfigList`] chains. Custom implementations are
/// rarely needed; build a [`LeafConfigList`] instead.
pub trait LeafConfigListCompatible<Configurer, ConfigType>
where
    Configurer: ConfigureElements<ConfigType>,
{
    /// Applies a leaf config blob to an open leaf element.
    fn configure(
        data: &LeafConfigData,
        configurer: &mut OpenLeafElement<'_, Configurer, ConfigType>,
    );

    /// Applies a leaf config blob to an open element (consuming it).
    fn configure_open(data: &LeafConfigData, configurer: OpenElement<'_, Configurer, ConfigType>);
}

impl<Head, Tail, Configurer, ConfigType> LeafConfigListCompatible<Configurer, ConfigType>
    for LeafConfigList<Head, Tail>
where
    Configurer: ConfigureLeafElements<Head> + ConfigureElements<ConfigType>,
    Tail: LeafConfigListCompatible<Configurer, ConfigType>,
    Head: DeserializeOwned,
{
    fn configure(
        data: &LeafConfigData,
        configurer: &mut OpenLeafElement<'_, Configurer, ConfigType>,
    ) {
        let reconstruction: Option<Head> = data.try_reconstruct();
        match reconstruction {
            Some(reconstruction) => {
                configurer.set_leaf_config(reconstruction);
            }
            None => {
                Tail::configure(data, configurer);
            }
        }
    }

    fn configure_open(data: &LeafConfigData, configurer: OpenElement<'_, Configurer, ConfigType>) {
        let reconstruction: Option<Head> = data.try_reconstruct();
        match reconstruction {
            Some(reconstruction) => {
                configurer.set_leaf_config(reconstruction);
            }
            None => {
                Tail::configure_open(data, configurer);
            }
        }
    }
}

impl<Configurer, ConfigType> LeafConfigListCompatible<Configurer, ConfigType> for LeafConfigListNil
where
    Configurer: ConfigureElements<ConfigType>,
{
    fn configure(
        _data: &LeafConfigData,
        _configurer: &mut OpenLeafElement<'_, Configurer, ConfigType>,
    ) {
    }

    fn configure_open(_data: &LeafConfigData, configurer: OpenElement<'_, Configurer, ConfigType>) {
        drop(configurer);
    }
}