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    pub(crate) fn set_children(&self, new_children: Vec<Entry>) {
140        let old_children = {
141            let mut state = self.state();
142            std::mem::replace(&mut state.children, new_children.clone())
143        };
144        for child in &new_children {
145            child.state().parent = Some(Arc::downgrade(&self.inner));
146        }
147        for child in &old_children {
148            let still_present = new_children.iter().any(|kept| Entry::ptr_eq(kept, child));
149            if !still_present {
150                child.state().parent = None;
151            }
152        }
153    }
154
155    /// Composite id from the tree root down to this entry, e.g. `a1:b2`.
156    pub fn path(&self) -> String {
157        let mut parts = vec![self.inner.id.clone()];
158        let mut current = self.parent();
159        while let Some(parent) = current {
160            if parent.inner.id.is_empty() {
161                break;
162            }
163            parts.push(parent.inner.id.clone());
164            current = parent.parent();
165        }
166        parts.reverse();
167        parts.join(":")
168    }
169
170    /// The entry's own disable slot, statically viewed: [`Disabled::Flag`]
171    /// yields the flag, an unevaluated expression does not disable. The
172    /// options snapshot ([`Entry::options`]) carries the raw slot for
173    /// write-back.
174    pub fn disabled(&self) -> bool {
175        self.state().options.disabled.is_disabled()
176    }
177
178    /// The entry's own disable state with its `!!js` expression evaluated:
179    /// the expression must evaluate to a boolean, or the error propagates.
180    pub fn resolved_disabled(&self) -> Result<bool> {
181        let disabled = self.state().options.disabled.clone();
182        match disabled {
183            Disabled::Flag(flag) => Ok(flag),
184            Disabled::Expr(source) => match crate::expr::evaluate(&source)? {
185                crate::node::Node::Bool(flag) => Ok(flag),
186                other => Err(IncludeError::JsExpression {
187                    message: format!(
188                        "the disabled expression must evaluate to a boolean, found {}",
189                        crate::yaml::node_kind(&other)
190                    ),
191                    expression: source,
192                }),
193            },
194        }
195    }
196
197    /// Whether this entry and every ancestor are statically enabled. A
198    /// disabled group cascades to its whole subtree. `!!js` expressions
199    /// are *not* evaluated here — an unevaluated expression does not
200    /// disable; use [`Entry::resolved_enabled`] for the evaluated
201    /// decision.
202    pub fn enabled(&self) -> bool {
203        if self.is_root() {
204            return true;
205        }
206        !self.disabled() && self.parent().is_none_or(|parent| parent.enabled())
207    }
208
209    /// Whether this entry and every ancestor are enabled once their
210    /// `!!js` expressions are evaluated (a disabled group cascades to
211    /// its whole subtree). The first evaluation failure propagates.
212    pub fn resolved_enabled(&self) -> Result<bool> {
213        if self.is_root() {
214            return Ok(true);
215        }
216        if self.resolved_disabled()? {
217            return Ok(false);
218        }
219        match self.parent() {
220            Some(parent) => parent.resolved_enabled(),
221            None => Ok(true),
222        }
223    }
224
225    /// The fiber started for this entry, if any (set by the loader layer).
226    pub fn fiber(&self) -> Option<Fiber> {
227        self.state().fiber.clone()
228    }
229
230    /// Attach or detach the entry's fiber.
231    pub fn set_fiber(&self, fiber: Option<Fiber>) {
232        self.state().fiber = fiber;
233    }
234
235    /// Increment the suspend counter, returning a guard. While suspended,
236    /// the loader suppresses config write-back for this entry.
237    pub fn suspend(&self) -> EntrySuspendGuard {
238        {
239            let mut state = self.state();
240            state.suspend += 1;
241        }
242        EntrySuspendGuard {
243            entry: self.clone(),
244        }
245    }
246
247    /// Whether any suspend guard is currently held for this entry.
248    pub fn is_suspended(&self) -> bool {
249        self.state().suspend > 0
250    }
251
252    pub(crate) fn is_root(&self) -> bool {
253        self.inner.id.is_empty()
254    }
255
256    /// Whether `other` is this entry or one of its descendants.
257    pub(crate) fn contains(&self, other: &Entry) -> bool {
258        if Entry::ptr_eq(self, other) {
259            return true;
260        }
261        other.parent().is_some_and(|parent| self.contains(&parent))
262    }
263}
264
265impl std::fmt::Debug for Entry {
266    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267        f.debug_struct("Entry")
268            .field("id", &self.inner.id)
269            .field("name", &self.name())
270            .finish_non_exhaustive()
271    }
272}
273
274/// RAII guard for the entry-level suspend counter.
275///
276/// Dropping the guard decrements the counter; write-back resumes once all
277/// guards for the entry are gone.
278#[derive(Debug)]
279pub struct EntrySuspendGuard {
280    entry: Entry,
281}
282
283impl Drop for EntrySuspendGuard {
284    fn drop(&mut self) {
285        let mut state = self.entry.state();
286        state.suspend = state.suspend.saturating_sub(1);
287    }
288}