cordis-include 0.0.17

Config entry trees and YAML/JSON loader files for the cordis-rs plugin framework
Documentation

cordis-include

English | 简体中文

Config entry trees and YAML/JSON loader files for the cordis-rs plugin framework.

This crate is the data half of porting upstream Cordis' loader: it maps between config files on disk and an in-memory tree of entries, preserving object key order for diff-friendly files, expanding ${{ env.NAME }} templates, and providing the suspend guards that break the write → watch → write feedback loop.

┌─ cordis-loader   assembly: plugin registry + fiber state machine
├─ cordis-group    group plugin (nesting marker)
├─ cordis-include  ← this crate: entry trees + config files
└─ cordis-rs       core runtime (zero dependencies)

Example

use cordis_include::{Entry, EntryOptions, EntryTree, Node};
# fn main() -> cordis_include::Result<()> {
let tree = EntryTree::new();

// Load a set of entries (from a file, or built by hand).
let diff = tree.update(vec![
    EntryOptions::new("group").with_id("srv").with_group(vec![
        EntryOptions::new("adapter-http").with_config(
            [("port".to_string(), Node::Int(8080))].into_iter().collect(),
        ),
    ]),
])?;
assert_eq!(diff.created.len(), 2);

// A full reload matches entries by id across groups: existing entry
// objects are reused (same pointer), so callers can keep their handles.
let kept = tree.resolve("srv").unwrap();
tree.update(vec![EntryOptions::new("group").with_id("srv")])?;
assert!(Entry::ptr_eq(&kept, &tree.resolve("srv").unwrap()));
# Ok(())
# }

Files round-trip through LoaderFile with atomic, writer-serialized .tmp + rename writes (concurrent writers cannot interleave), readonly detection, unknown top-level keys preserved, and coalesced deferred writes (write_deferred) for bursty callers. YAML parses through the crate's own dialect: it matches the previous serde-based reader (verified by A/B tests) while keeping !!js scalars as expression nodes (Node) that round-trip verbatim, unevaluated:

entries:
  - id: srv
    name: group
    group:
      - name: adapter-http
        config:
          port: 8080
          host: ${{ env.HOST }}

Patch lists

Entry lists compose from patch files — bare top-level YAML arrays of PatchOptions rows (id-targeted overrides and insert lists), the bundle/profile assembly model. apply_entry_patches is the one application routine every consumer shares; compose_layers flattens all layers into a single call (the same single call a boot performs); render_config_dump prints the composition grouped by source under # == provenance comments:

use cordis_include::{compose_layers, EntryOptions, Node, PatchOptions};
# fn main() {
let bundle = vec![PatchOptions {
    insert: Some(vec![EntryOptions::new("adapter-http")
        .with_id("http")
        .with_config(Node::from_iter([("port".to_string(), 8080.into())]))]),
    ..Default::default()
}];
let user = vec![PatchOptions {
    id: Some("http".into()),
    disabled: Some(true),
    ..Default::default()
}];
let entries = compose_layers(&[bundle, user], |_| {});
assert_eq!(entries.len(), 1);
assert!(entries[0].disabled);
# }

Feature flags

  • watch — debounced file watching through notify. Events observed while the file is suspended — by a caller-held suspend guard, e.g. around the caller's own writes — do not fire the callback.

Scope

This crate deliberately knows nothing about where plugins come from and never starts or stops fibers: cordis-loader implements the PluginResolver contract defined here and drives the lifecycle.