Skip to main content

cordis_include/
entry.rs

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