Skip to main content

cordis_include/
lib.rs

1//! Config entry trees and loader files for the
2//! [cordis-rs](https://crates.io/crates/cordis-rs) plugin framework.
3//!
4//! This crate is the data half of porting upstream Cordis' loader: it maps
5//! between config files on disk and an in-memory tree of
6//! [`Entry`] nodes, each described by [`EntryOptions`]. It deliberately
7//! knows nothing about *where plugins come from* and never starts or stops
8//! fibers — that is `cordis-loader`'s job, plugged in through the
9//! [`PluginResolver`] trait defined here.
10//!
11//! # Example
12//!
13//! ```no_run
14//! use cordis_include::{Document, EntryOptions, EntryTree, LoaderFile};
15//!
16//! # fn main() -> cordis_include::Result<()> {
17//! let file = LoaderFile::open("cordis.yml")?;
18//! let mut document = file.read()?;
19//!
20//! let tree = EntryTree::new();
21//! let diff = tree.update(document.entries)?;
22//! for entry in &diff.created {
23//!     println!("new entry {} ({})", entry.path(), entry.name());
24//! }
25//!
26//! // Persist generated ids and later edits back to the file.
27//! document.entries = tree.serialize();
28//! file.write(&document)?;
29//! # Ok(())
30//! # }
31//! ```
32//!
33//! # File format
34//!
35//! A file holds an ordered entry list; nested `group` arrays make groups.
36//! Object key order is preserved on round-trip, entry fields serialize as
37//! `id`, `name`, `disabled`, `inject`, `group`, `config` (config last), and
38//! unknown top-level keys are kept untouched — files stay diff-friendly.
39//!
40//! ```yaml
41//! entries:
42//!   - id: sched
43//!     name: group
44//!     group:
45//!       - name: adapter-http
46//!         config:
47//!           port: 8080
48//!           host: ${{ env.HOST }}
49//! ```
50//!
51//! `${{ env.NAME }}` templates substitute environment variables when config
52//! is handed to a plugin ([`Entry::resolved_config`]); the file itself keeps
53//! the template text. `!!js` scalars parse through the crate's own YAML
54//! dialect (the [`yaml`] module) and round-trip as expression nodes
55//! ([`Node::Expr`]), still unevaluated — evaluation of the expression
56//! subset is the loader's hand-off job.
57//!
58//! # Patch lists
59//!
60//! Entry lists compose from *patch* files — bare top-level YAML arrays of
61//! [`PatchOptions`] rows (`id`-targeted overrides and `insert` lists), the
62//! bundle/profile assembly model. See the [`patch`] module for the apply,
63//! composition, provenance, and dump mechanisms.
64//!
65//! # Suspension
66//!
67//! Two suspend counters break the reload feedback loop: a file-level guard
68//! ([`LoaderFile::suspend`]) suppresses physical writes, and an entry-level
69//! guard ([`Entry::suspend`]) tells the loader that an entry's changes came
70//! from the file and must not be written back. The `watch` feature adds
71//! [`FileWatcher`], a debounced watcher that skips events observed while
72//! the file is suspended.
73//!
74//! # Not in scope
75//!
76//! Plugin resolution beyond the [`PluginResolver`] contract (static
77//! registries and dynamic libraries live in `cordis-loader`), fiber
78//! lifecycle, and cascading group semantics (`cordis-group`).
79
80// `deny` instead of `forbid` because the YAML dialect module wraps
81// `unsafe-libyaml`'s C-translation parser; every unsafe operation lives in
82// that one module, item-scoped behind `#[allow(unsafe_code)]` with SAFETY
83// notes (the same pattern `cordis-loader` uses for libloading).
84#![deny(unsafe_code)]
85#![warn(missing_docs)]
86
87pub mod entry;
88pub mod error;
89pub mod file;
90pub mod interpolate;
91pub mod node;
92pub mod options;
93pub mod patch;
94pub mod resolver;
95pub mod tree;
96#[cfg(feature = "watch")]
97pub mod watch;
98pub mod yaml;
99
100pub use entry::{Entry, EntrySuspendGuard};
101pub use error::{IncludeError, Result};
102pub use file::{Document, FileFormat, FileSuspendGuard, LoaderFile};
103pub use node::{Node, NodeMap};
104pub use options::{EntryOptions, GROUP_NAME, IMPORT_NAME};
105pub use patch::{
106    DumpLayer, PatchOptions, Provenance, apply_entry_patches, compose_layers,
107    compose_with_provenance, load_optional_patches, load_overlay_patches, render_config_dump,
108    render_dump,
109};
110pub use resolver::PluginResolver;
111pub use tree::{EntryTree, RemovedEntry, TreeDiff};
112#[cfg(feature = "watch")]
113pub use watch::FileWatcher;
114pub use yaml::{emit_document, emit_entry_list, parse_document, parse_entry_list, parse_node};
115
116/// Lock a mutex tolerantly, treating a poisoned lock as unlocked.
117///
118/// Mirrors the pattern used inside `cordis-rs`: a panic in one thread must
119/// not cascade into `unwrap` failures elsewhere. The guarded state may be
120/// mid-update, which is acceptable for config trees.
121pub(crate) fn lock<T>(mutex: &std::sync::Mutex<T>) -> std::sync::MutexGuard<'_, T> {
122    mutex.lock().unwrap_or_else(|error| error.into_inner())
123}