Skip to main content

cordis_include/
entry.rs

1//! In-memory entry nodes: identity, runtime state, and ancestor walks.
2
3use crate::error::{IncludeError, Result};
4use crate::expr::evaluate_node;
5use crate::interpolate::interpolate_node;
6use crate::lock;
7use crate::options::{Disabled, EntryOptions};
8use cordis::Fiber;
9use std::sync::{Arc, Mutex, MutexGuard, Weak};
10
11/// A live entry in an [`crate::EntryTree`].
12///
13/// `Entry` is a cheap handle (interior `Arc`); two handles refer to the same
14/// entry exactly when [`Entry::ptr_eq`] says so. The loader attaches the
15/// entry's [`Fiber`] here and uses [`Entry::suspend`] to suppress write-back
16/// while applying changes that originate from the config file itself.
17#[derive(Clone)]
18pub struct Entry {
19    inner: Arc<EntryInner>,
20}
21
22/// Immutable identity plus guarded runtime state of one entry.
23struct EntryInner {
24    id: String,
25    state: Mutex<EntryState>,
26}
27
28/// Fields that mutate over the entry's lifetime.
29pub(crate) struct EntryState {
30    options: EntryOptions,
31    parent: Option<Weak<EntryInner>>,
32    children: Vec<Entry>,
33    fiber: Option<Fiber>,
34    suspend: usize,
35}
36
37impl Entry {
38    /// Create a detached leaf entry; the tree wires parent and children.
39    pub(crate) fn new(id: String, options: EntryOptions) -> Self {
40        debug_assert_eq!(options.id.as_deref(), Some(id.as_str()));
41        Self {
42            inner: Arc::new(EntryInner {
43                id,
44                state: Mutex::new(EntryState {
45                    options,
46                    parent: None,
47                    children: Vec::new(),
48                    fiber: None,
49                    suspend: 0,
50                }),
51            }),
52        }
53    }
54
55    /// The synthetic tree root, addressed by the empty id.
56    pub(crate) fn new_root() -> Self {
57        Self::new(
58            String::new(),
59            EntryOptions {
60                id: Some(String::new()),
61                ..EntryOptions::default()
62            },
63        )
64    }
65
66    /// Stable identity of this entry; never empty except for the root.
67    pub fn id(&self) -> &str {
68        &self.inner.id
69    }
70
71    /// Whether both handles refer to the same entry.
72    pub fn ptr_eq(left: &Entry, right: &Entry) -> bool {
73        Arc::ptr_eq(&left.inner, &right.inner)
74    }
75
76    /// Borrow the guarded runtime state.
77    pub(crate) fn state(&self) -> MutexGuard<'_, EntryState> {
78        lock(&self.inner.state)
79    }
80
81    /// The plugin name from the entry options.
82    pub fn name(&self) -> String {
83        self.state().options.name.clone()
84    }
85
86    /// A snapshot of the raw entry options (config templates unexpanded,
87    /// `group` always empty — children are live tree state).
88    pub fn options(&self) -> EntryOptions {
89        let mut options = self.state().options.clone();
90        options.group = Vec::new();
91        options
92    }
93
94    /// Replace the entry options. The id is identity and never changes.
95    pub(crate) fn set_options(&self, options: EntryOptions) {
96        let mut state = self.state();
97        state.options = options;
98        state.options.id = Some(self.inner.id.clone());
99        state.options.group = Vec::new();
100    }
101
102    /// The raw config with `${{ ... }}` templates still intact.
103    pub fn config(&self) -> Option<crate::node::Node> {
104        self.state().options.config.clone()
105    }
106
107    /// The config with `${{ env.NAME }}` templates expanded and every
108    /// `!!js` expression node evaluated, ready to be handed to a plugin
109    /// as `cordis_rs::Value::new(node)`. An out-of-subset expression
110    /// (for example one referencing `ctx.*`) fails here.
111    pub fn resolved_config(&self) -> Result<Option<crate::node::Node>> {
112        match self.config() {
113            Some(node) => evaluate_node(&interpolate_node(&node)?).map(Some),
114            None => Ok(None),
115        }
116    }
117
118    /// The parent entry, or `None` for the tree root and detached entries.
119    pub fn parent(&self) -> Option<Entry> {
120        let parent = self.state().parent.clone();
121        parent
122            .and_then(|weak| weak.upgrade())
123            .map(|inner| Entry { inner })
124    }
125
126    /// Child entries in file order. Empty for leaf entries.
127    pub fn children(&self) -> Vec<Entry> {
128        self.state().children.clone()
129    }
130
131    /// Whether this entry currently has children (i.e. acts as a group).
132    pub fn is_group(&self) -> bool {
133        !self.state().children.is_empty()
134    }
135
136    /// Replace the child list, re-pointing moved-in children and clearing
137    /// the parent link of children that were dropped. Detached children
138    /// keep their own subtrees intact for teardown.
139    ///
140    /// A dropped child that is no longer *ours* is left alone: during a
141    /// whole-tree update a nested `set_children` pass may have re-parented
142    /// it already (a top-level entry moved into a group), and clearing
143    /// that fresh link would orphan the entry from ancestor cascades.
144    pub(crate) fn set_children(&self, new_children: Vec<Entry>) {
145        let old_children = {
146            let mut state = self.state();
147            std::mem::replace(&mut state.children, new_children.clone())
148        };
149        for child in &new_children {
150            child.state().parent = Some(Arc::downgrade(&self.inner));
151        }
152        for child in &old_children {
153            let still_present = new_children.iter().any(|kept| Entry::ptr_eq(kept, child));
154            if !still_present && child.parent().is_some_and(|p| Entry::ptr_eq(&p, self)) {
155                child.state().parent = None;
156            }
157        }
158    }
159
160    /// Composite id from the tree root down to this entry, e.g. `a1:b2`.
161    pub fn path(&self) -> String {
162        let mut parts = vec![self.inner.id.clone()];
163        let mut current = self.parent();
164        while let Some(parent) = current {
165            if parent.inner.id.is_empty() {
166                break;
167            }
168            parts.push(parent.inner.id.clone());
169            current = parent.parent();
170        }
171        parts.reverse();
172        parts.join(":")
173    }
174
175    /// The entry's own disable slot, statically viewed: [`Disabled::Flag`]
176    /// yields the flag, an unevaluated expression does not disable. The
177    /// options snapshot ([`Entry::options`]) carries the raw slot for
178    /// write-back.
179    pub fn disabled(&self) -> bool {
180        self.state().options.disabled.is_disabled()
181    }
182
183    /// The entry's own disable state with its `!!js` expression evaluated:
184    /// the expression must evaluate to a boolean, or the error propagates.
185    pub fn resolved_disabled(&self) -> Result<bool> {
186        let disabled = self.state().options.disabled.clone();
187        match disabled {
188            Disabled::Flag(flag) => Ok(flag),
189            Disabled::Expr(source) => match crate::expr::evaluate(&source)? {
190                crate::node::Node::Bool(flag) => Ok(flag),
191                other => Err(IncludeError::JsExpression {
192                    message: format!(
193                        "the disabled expression must evaluate to a boolean, found {}",
194                        crate::yaml::node_kind(&other)
195                    ),
196                    expression: source,
197                }),
198            },
199        }
200    }
201
202    /// Whether this entry and every ancestor are statically enabled. A
203    /// disabled group cascades to its whole subtree. `!!js` expressions
204    /// are *not* evaluated here — an unevaluated expression does not
205    /// disable; use [`Entry::resolved_enabled`] for the evaluated
206    /// decision.
207    pub fn enabled(&self) -> bool {
208        if self.is_root() {
209            return true;
210        }
211        !self.disabled() && self.parent().is_none_or(|parent| parent.enabled())
212    }
213
214    /// Whether this entry and every ancestor are enabled once their
215    /// `!!js` expressions are evaluated (a disabled group cascades to
216    /// its whole subtree). The first evaluation failure propagates.
217    pub fn resolved_enabled(&self) -> Result<bool> {
218        if self.is_root() {
219            return Ok(true);
220        }
221        if self.resolved_disabled()? {
222            return Ok(false);
223        }
224        match self.parent() {
225            Some(parent) => parent.resolved_enabled(),
226            None => Ok(true),
227        }
228    }
229
230    /// The fiber started for this entry, if any (set by the loader layer).
231    pub fn fiber(&self) -> Option<Fiber> {
232        self.state().fiber.clone()
233    }
234
235    /// Attach or detach the entry's fiber.
236    pub fn set_fiber(&self, fiber: Option<Fiber>) {
237        self.state().fiber = fiber;
238    }
239
240    /// Increment the suspend counter, returning a guard. While suspended,
241    /// the loader suppresses config write-back for this entry.
242    pub fn suspend(&self) -> EntrySuspendGuard {
243        {
244            let mut state = self.state();
245            state.suspend += 1;
246        }
247        EntrySuspendGuard {
248            entry: self.clone(),
249        }
250    }
251
252    /// Whether any suspend guard is currently held for this entry.
253    pub fn is_suspended(&self) -> bool {
254        self.state().suspend > 0
255    }
256
257    pub(crate) fn is_root(&self) -> bool {
258        self.inner.id.is_empty()
259    }
260
261    /// Whether `other` is this entry or one of its descendants.
262    pub(crate) fn contains(&self, other: &Entry) -> bool {
263        if Entry::ptr_eq(self, other) {
264            return true;
265        }
266        other.parent().is_some_and(|parent| self.contains(&parent))
267    }
268}
269
270impl std::fmt::Debug for Entry {
271    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272        f.debug_struct("Entry")
273            .field("id", &self.inner.id)
274            .field("name", &self.name())
275            .finish_non_exhaustive()
276    }
277}
278
279/// RAII guard for the entry-level suspend counter.
280///
281/// Dropping the guard decrements the counter; write-back resumes once all
282/// guards for the entry are gone.
283#[derive(Debug)]
284pub struct EntrySuspendGuard {
285    entry: Entry,
286}
287
288impl Drop for EntrySuspendGuard {
289    fn drop(&mut self) {
290        let mut state = self.entry.state();
291        state.suspend = state.suspend.saturating_sub(1);
292    }
293}