cordis-include 0.0.6

Config entry trees and YAML/JSON loader files for the cordis-rs plugin framework
Documentation
//! In-memory entry nodes: identity, runtime state, and ancestor walks.

use crate::error::Result;
use crate::interpolate::interpolate_node;
use crate::lock;
use crate::options::EntryOptions;
use cordis::Fiber;
use std::sync::{Arc, Mutex, MutexGuard, Weak};

/// A live entry in an [`crate::EntryTree`].
///
/// `Entry` is a cheap handle (interior `Arc`); two handles refer to the same
/// entry exactly when [`Entry::ptr_eq`] says so. The loader attaches the
/// entry's [`Fiber`] here and uses [`Entry::suspend`] to suppress write-back
/// while applying changes that originate from the config file itself.
#[derive(Clone)]
pub struct Entry {
    inner: Arc<EntryInner>,
}

/// Immutable identity plus guarded runtime state of one entry.
struct EntryInner {
    id: String,
    state: Mutex<EntryState>,
}

/// Fields that mutate over the entry's lifetime.
pub(crate) struct EntryState {
    options: EntryOptions,
    parent: Option<Weak<EntryInner>>,
    children: Vec<Entry>,
    fiber: Option<Fiber>,
    suspend: usize,
}

impl Entry {
    /// Create a detached leaf entry; the tree wires parent and children.
    pub(crate) fn new(id: String, options: EntryOptions) -> Self {
        debug_assert_eq!(options.id.as_deref(), Some(id.as_str()));
        Self {
            inner: Arc::new(EntryInner {
                id,
                state: Mutex::new(EntryState {
                    options,
                    parent: None,
                    children: Vec::new(),
                    fiber: None,
                    suspend: 0,
                }),
            }),
        }
    }

    /// The synthetic tree root, addressed by the empty id.
    pub(crate) fn new_root() -> Self {
        Self::new(
            String::new(),
            EntryOptions {
                id: Some(String::new()),
                ..EntryOptions::default()
            },
        )
    }

    /// Stable identity of this entry; never empty except for the root.
    pub fn id(&self) -> &str {
        &self.inner.id
    }

    /// Whether both handles refer to the same entry.
    pub fn ptr_eq(left: &Entry, right: &Entry) -> bool {
        Arc::ptr_eq(&left.inner, &right.inner)
    }

    /// Borrow the guarded runtime state.
    pub(crate) fn state(&self) -> MutexGuard<'_, EntryState> {
        lock(&self.inner.state)
    }

    /// The plugin name from the entry options.
    pub fn name(&self) -> String {
        self.state().options.name.clone()
    }

    /// A snapshot of the raw entry options (config templates unexpanded,
    /// `group` always empty — children are live tree state).
    pub fn options(&self) -> EntryOptions {
        let mut options = self.state().options.clone();
        options.group = Vec::new();
        options
    }

    /// Replace the entry options. The id is identity and never changes.
    pub(crate) fn set_options(&self, options: EntryOptions) {
        let mut state = self.state();
        state.options = options;
        state.options.id = Some(self.inner.id.clone());
        state.options.group = Vec::new();
    }

    /// The raw config with `${{ ... }}` templates still intact.
    pub fn config(&self) -> Option<crate::node::Node> {
        self.state().options.config.clone()
    }

    /// The config with `${{ env.NAME }}` templates expanded, ready to be
    /// handed to a plugin as `cordis_rs::Value::new(node)`.
    pub fn resolved_config(&self) -> Result<Option<crate::node::Node>> {
        match self.config() {
            Some(node) => interpolate_node(&node).map(Some),
            None => Ok(None),
        }
    }

    /// The parent entry, or `None` for the tree root and detached entries.
    pub fn parent(&self) -> Option<Entry> {
        let parent = self.state().parent.clone();
        parent
            .and_then(|weak| weak.upgrade())
            .map(|inner| Entry { inner })
    }

    /// Child entries in file order. Empty for leaf entries.
    pub fn children(&self) -> Vec<Entry> {
        self.state().children.clone()
    }

    /// Whether this entry currently has children (i.e. acts as a group).
    pub fn is_group(&self) -> bool {
        !self.state().children.is_empty()
    }

    /// Replace the child list, re-pointing moved-in children and clearing
    /// the parent link of children that were dropped. Detached children
    /// keep their own subtrees intact for teardown.
    pub(crate) fn set_children(&self, new_children: Vec<Entry>) {
        let old_children = {
            let mut state = self.state();
            std::mem::replace(&mut state.children, new_children.clone())
        };
        for child in &new_children {
            child.state().parent = Some(Arc::downgrade(&self.inner));
        }
        for child in &old_children {
            let still_present = new_children.iter().any(|kept| Entry::ptr_eq(kept, child));
            if !still_present {
                child.state().parent = None;
            }
        }
    }

    /// Composite id from the tree root down to this entry, e.g. `a1:b2`.
    pub fn path(&self) -> String {
        let mut parts = vec![self.inner.id.clone()];
        let mut current = self.parent();
        while let Some(parent) = current {
            if parent.inner.id.is_empty() {
                break;
            }
            parts.push(parent.inner.id.clone());
            current = parent.parent();
        }
        parts.reverse();
        parts.join(":")
    }

    /// Whether this entry is itself flagged as disabled.
    pub fn disabled(&self) -> bool {
        self.state().options.disabled
    }

    /// Whether the entry and every ancestor are enabled. A disabled group
    /// cascades to its whole subtree.
    pub fn enabled(&self) -> bool {
        if self.is_root() {
            return true;
        }
        !self.disabled() && self.parent().is_none_or(|parent| parent.enabled())
    }

    /// The fiber started for this entry, if any (set by the loader layer).
    pub fn fiber(&self) -> Option<Fiber> {
        self.state().fiber.clone()
    }

    /// Attach or detach the entry's fiber.
    pub fn set_fiber(&self, fiber: Option<Fiber>) {
        self.state().fiber = fiber;
    }

    /// Increment the suspend counter, returning a guard. While suspended,
    /// the loader suppresses config write-back for this entry.
    pub fn suspend(&self) -> EntrySuspendGuard {
        {
            let mut state = self.state();
            state.suspend += 1;
        }
        EntrySuspendGuard {
            entry: self.clone(),
        }
    }

    /// Whether any suspend guard is currently held for this entry.
    pub fn is_suspended(&self) -> bool {
        self.state().suspend > 0
    }

    pub(crate) fn is_root(&self) -> bool {
        self.inner.id.is_empty()
    }

    /// Whether `other` is this entry or one of its descendants.
    pub(crate) fn contains(&self, other: &Entry) -> bool {
        if Entry::ptr_eq(self, other) {
            return true;
        }
        other.parent().is_some_and(|parent| self.contains(&parent))
    }
}

impl std::fmt::Debug for Entry {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Entry")
            .field("id", &self.inner.id)
            .field("name", &self.name())
            .finish_non_exhaustive()
    }
}

/// RAII guard for the entry-level suspend counter.
///
/// Dropping the guard decrements the counter; write-back resumes once all
/// guards for the entry are gone.
#[derive(Debug)]
pub struct EntrySuspendGuard {
    entry: Entry,
}

impl Drop for EntrySuspendGuard {
    fn drop(&mut self) {
        let mut state = self.entry.state();
        state.suspend = state.suspend.saturating_sub(1);
    }
}