Skip to main content

cordis_loader/
lib.rs

1//! Config-file driven plugin loader for
2//! [cordis-rs](https://crates.io/crates/cordis-rs).
3//!
4//! This crate is the assembly half of porting upstream Cordis' loader: it
5//! connects `cordis-include`'s entry trees to cordis fibers. Everything a
6//! config-driven cordis application needs is re-exported here — depend on
7//! `cordis-loader` alone.
8//!
9//! # How it works
10//!
11//! - **Static registry** ([`PluginRegistry`]) replaces upstream's dynamic
12//!   `import(name)`: register plugins at startup, entries resolve by name.
13//!   The `group` builtin is pre-registered.
14//! - **Startup**: [`Loader::open`] reads the entry file (writing
15//!   `initial` when missing), builds the [`EntryTree`], and starts every
16//!   enabled entry — group entries as `cordis_group::Group` fibers, children
17//!   beneath their parent group's context, so disposing a group cascades.
18//! - **Config**: entries carry a `cordis_include::Node` config (never `()`);
19//!   loader plugins read it via `config.downcast::<Node>()`. `${{ env.X }}`
20//!   templates expand at hand-off time; the file keeps the raw text.
21//! - **Reload** ([`Loader::reload`], wired to the `watch` feature):
22//!   re-read the file, diff the tree, and reconcile fibers — created entries
23//!   start, removed subtrees stop, moved entries restart under their new
24//!   parent, config-only changes patch in place via `Fiber::update_value`.
25//! - **Inject**: an entry's `inject` list is merged into the plugin's own
26//!   declaration, so the core fiber machinery reconciles entries when
27//!   services come and go — "hot-swapped service restarts its dependents"
28//!   for free.
29//! - **Self-kill vs. removal**: a fiber that reaches `Disposed` outside
30//!   loader operation was killed by its own plugin; the loader persists
31//!   `disabled: true` for that entry. Removing an entry from the file just
32//!   stops it.
33//! - **Write-back**: [`Loader::update_config`] is the runtime entry point —
34//!   it updates the fiber *and* persists the config. Reloads never echo
35//!   back (file-level suspend).
36//!
37//! # Example
38//!
39//! ```
40//! use cordis_loader::{Loader, LoaderConfig, PluginRegistry};
41//!
42//! let root = cordis::Context::new();
43//! let mut registry = PluginRegistry::new();
44//! // registry.register_plugin(my_plugin);  // your plugins, by name
45//! let config = LoaderConfig::new("cordis.yml").with_registry(registry);
46//! let loader = Loader::open(&root, config)?;
47//! # assert!(loader.tree().entries().is_empty());
48//! # Ok::<(), cordis_loader::LoaderError>(())
49//! ```
50//!
51//! Register the plugins first (the `group` builtin is pre-registered), then
52//! open; plugins registered later via [`Loader::register_plugin`] are picked
53//! up by the next [`Loader::reload`].
54//!
55//! # Not in scope yet
56//!
57//! The `import` entry kind (mounting a sub-file), isolate/service
58//! migration, the `loader/entry-init`-style event family, and debounced
59//! merged writes are future work.
60
61#![forbid(unsafe_code)]
62#![warn(missing_docs)]
63
64pub mod error;
65pub mod loader;
66pub mod registry;
67
68pub use cordis_group::Group;
69pub use cordis_include::{Document, Entry, EntryOptions, EntryTree, LoaderFile, Node};
70pub use error::{LoaderError, Result};
71pub use loader::{Loader, LoaderConfig, LoaderHandle};
72pub use registry::PluginRegistry;
73
74/// Lock a mutex tolerantly, treating a poisoned lock as unlocked.
75pub(crate) fn lock<T>(mutex: &std::sync::Mutex<T>) -> std::sync::MutexGuard<'_, T> {
76    mutex.lock().unwrap_or_else(|error| error.into_inner())
77}