use etdl_parser::ast::EtlDocument;
use etdl_tree_core::{Tree, TreeError};
use crate::validate::Diagnostic;
const TREE_EVENT_SUPPLEMENT: &str = "etdl.tree-event";
pub fn parse_and_validate_trees(doc: &EtlDocument) -> (Vec<Tree>, Vec<Diagnostic>) {
let mut diagnostics = Vec::new();
let mut trees = Vec::new();
if !crate::validate::declares_supplement(doc, TREE_EVENT_SUPPLEMENT) {
return (trees, diagnostics);
}
let Some(ext) = doc.extensions.get("x-tree-event") else {
return (trees, diagnostics);
};
let raw_trees = match ext.get("trees") {
Some(v) => v,
None => {
diagnostics.push(Diagnostic::error(
"E-120",
"x-tree-event: missing required 'trees' field".to_string(),
));
return (trees, diagnostics);
}
};
let parsed: Result<Vec<Tree>, _> = serde_yaml::from_value(raw_trees.clone());
let candidates = match parsed {
Ok(t) => t,
Err(e) => {
diagnostics.push(Diagnostic::error(
"E-120",
format!("x-tree-event: invalid tree manifest: {e}"),
));
return (trees, diagnostics);
}
};
let mut seen_ids = std::collections::BTreeSet::new();
for tree in candidates {
if !seen_ids.insert(tree.id.clone()) {
diagnostics.push(Diagnostic::error(
"E-122",
format!("x-tree-event: duplicate tree id '{}'", tree.id),
));
continue;
}
match tree.validate() {
Ok(()) => trees.push(tree),
Err(errors) => {
for e in errors {
diagnostics.push(Diagnostic::error("E-121", format_tree_error(&e)));
}
}
}
}
(trees, diagnostics)
}
fn format_tree_error(e: &TreeError) -> String {
format!("x-tree-event: {e}")
}
#[derive(Debug, Default)]
pub struct TreeEventExtension;
impl TreeEventExtension {
pub fn new() -> Self {
TreeEventExtension
}
}
pub struct TreeEventResult {
pub trees: Vec<Tree>,
}
impl crate::extension::ExtensionResult for TreeEventResult {
fn extension_id(&self) -> &str {
TREE_EVENT_SUPPLEMENT
}
}
impl crate::extension::EtdlExtension for TreeEventExtension {
fn id(&self) -> &str {
TREE_EVENT_SUPPLEMENT
}
fn version(&self) -> &str {
"1.0"
}
fn descriptor(&self) -> crate::extension::SupplementDescriptor {
crate::extension::SupplementDescriptor {
summary: "Domain-neutral tree-of-events structure — nodes, logical gates \
(AND/OR/NOT/XOR/K_OF_N), validation, traversal — for reliability, safety, \
security, and future domains to each interpret independently.",
schema: Some(etdl_tree_core::TREE_SCHEMA),
diagnostic_codes: &["E-120", "E-121", "E-122"],
requires: &[],
}
}
fn validate(
&self,
doc: &EtlDocument,
_context: &crate::extension::ExtensionContext<'_>,
diagnostics: &mut Vec<Diagnostic>,
) {
let (_trees, tree_diagnostics) = parse_and_validate_trees(doc);
diagnostics.extend(tree_diagnostics);
}
fn process(
&self,
doc: &EtlDocument,
_context: &crate::extension::ExtensionContext<'_>,
diagnostics: &mut Vec<Diagnostic>,
) -> Box<dyn crate::extension::ExtensionResult + '_> {
let (trees, tree_diagnostics) = parse_and_validate_trees(doc);
diagnostics.extend(tree_diagnostics);
Box::new(TreeEventResult { trees })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::extension::{builtin_registry, EtdlExtension, ExtensionContext};
fn doc_with_trees(x_tree_event_yaml: &str) -> EtlDocument {
let yaml = format!(
r#"
etdl: "1.0.0"
info: {{ title: "T", version: "1.0.0", domain: "D" }}
supplements:
- id: etdl.tree-event
version: "1.0"
eventTrees:
T:
initiatingEvent: {{ id: I, message: "a#/m", next: C }}
nodes:
C: {{ type: consequence, operation: terminate }}
x-tree-event:
{x_tree_event_yaml}
"#
);
serde_yaml::from_str(&yaml).unwrap()
}
#[test]
fn tree_event_extension_is_registered_and_built_in() {
let registry = builtin_registry();
assert!(registry.contains(TREE_EVENT_SUPPLEMENT));
assert!(registry.list().contains(&TREE_EVENT_SUPPLEMENT));
}
#[test]
fn valid_tree_parses_and_processes() {
let doc = doc_with_trees(
r#" trees:
- id: "demo"
version: "1"
root: "A"
nodes:
A:
kind: leaf
"#,
);
let ext = TreeEventExtension::new();
let base = std::path::Path::new(".");
let ctx = ExtensionContext::new(&doc, base);
let mut diagnostics = Vec::new();
let result = ext.process(&doc, &ctx, &mut diagnostics);
assert!(diagnostics.is_empty(), "unexpected: {diagnostics:?}");
assert_eq!(result.extension_id(), TREE_EVENT_SUPPLEMENT);
}
#[test]
fn cyclic_tree_produces_e121() {
let doc = doc_with_trees(
r#" trees:
- id: "demo"
version: "1"
root: "A"
nodes:
A:
kind: gate
gate: NOT
children: ["B"]
B:
kind: gate
gate: NOT
children: ["A"]
"#,
);
let ext = TreeEventExtension::new();
let base = std::path::Path::new(".");
let ctx = ExtensionContext::new(&doc, base);
let mut diagnostics = Vec::new();
ext.validate(&doc, &ctx, &mut diagnostics);
assert!(diagnostics.iter().any(|d| d.code == "E-121" && d.message.contains("cycle")));
}
#[test]
fn duplicate_tree_id_produces_e122() {
let doc = doc_with_trees(
r#" trees:
- id: "demo"
version: "1"
root: "A"
nodes:
A: { kind: leaf }
- id: "demo"
version: "2"
root: "B"
nodes:
B: { kind: leaf }
"#,
);
let ext = TreeEventExtension::new();
let base = std::path::Path::new(".");
let ctx = ExtensionContext::new(&doc, base);
let mut diagnostics = Vec::new();
ext.validate(&doc, &ctx, &mut diagnostics);
assert!(diagnostics.iter().any(|d| d.code == "E-122"));
}
#[test]
fn document_without_x_tree_event_has_no_trees_and_no_diagnostics() {
let yaml = r#"
etdl: "1.0.0"
info: { title: "T", version: "1.0.0", domain: "D" }
eventTrees:
T:
initiatingEvent: { id: I, message: "a#/m", next: C }
nodes:
C: { type: consequence, operation: terminate }
"#;
let doc: EtlDocument = serde_yaml::from_str(yaml).unwrap();
let (trees, diagnostics) = parse_and_validate_trees(&doc);
assert!(trees.is_empty());
assert!(diagnostics.is_empty());
}
}