use etdl_parser::ast::EtlDocument;
use std::collections::BTreeMap;
use crate::validate::Diagnostic;
#[derive(Debug, Clone)]
pub struct ExtensionContext<'a> {
pub doc: &'a EtlDocument,
pub base_dir: &'a std::path::Path,
pub config: BTreeMap<String, serde_yaml::Value>,
}
impl<'a> ExtensionContext<'a> {
pub fn new(doc: &'a EtlDocument, base_dir: &'a std::path::Path) -> Self {
ExtensionContext {
doc,
base_dir,
config: BTreeMap::new(),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SupplementDescriptor {
pub summary: &'static str,
pub schema: Option<&'static str>,
pub diagnostic_codes: &'static [&'static str],
pub requires: &'static [&'static str],
}
pub trait EtdlExtension: Send + Sync {
fn id(&self) -> &str;
fn version(&self) -> &str;
fn descriptor(&self) -> SupplementDescriptor {
SupplementDescriptor::default()
}
fn validate(
&self,
doc: &EtlDocument,
context: &ExtensionContext<'_>,
diagnostics: &mut Vec<Diagnostic>,
);
fn process(
&self,
_doc: &EtlDocument,
_context: &ExtensionContext<'_>,
_diagnostics: &mut Vec<Diagnostic>,
) -> Box<dyn ExtensionResult + '_> {
Box::new(NoopExtensionResult)
}
}
pub trait ExtensionResult {
fn extension_id(&self) -> &str;
fn basic_event_overrides(&self) -> Vec<(String, f64)> {
Vec::new()
}
}
pub struct NoopExtensionResult;
impl ExtensionResult for NoopExtensionResult {
fn extension_id(&self) -> &str {
""
}
}
#[derive(Default)]
pub struct ExtensionRegistry {
extensions: BTreeMap<String, Box<dyn EtdlExtension>>,
}
impl ExtensionRegistry {
pub fn new() -> Self {
ExtensionRegistry::default()
}
pub fn register<E: EtdlExtension + 'static>(&mut self, extension: E) {
self.extensions
.insert(extension.id().to_string(), Box::new(extension));
}
pub fn lookup(&self, id: &str) -> Option<&dyn EtdlExtension> {
self.extensions.get(id).map(|b| b.as_ref())
}
pub fn contains(&self, id: &str) -> bool {
self.extensions.contains_key(id)
}
pub fn list(&self) -> Vec<&str> {
self.extensions.keys().map(|s| s.as_str()).collect()
}
}
pub fn builtin_registry() -> ExtensionRegistry {
let mut registry = ExtensionRegistry::new();
registry.register(crate::tree_event::TreeEventExtension::new());
registry.register(crate::performance::PerformanceExtension::new());
registry.register(crate::safety::SafetyExtension::new());
registry.register(crate::diagnostics::DiagnosticsExtension::new());
registry.register(crate::security::SecurityExtension::new());
#[cfg(feature = "reliability")]
{
registry.register(crate::reliability::ReliabilityExtension::new());
}
registry
}
#[cfg(test)]
mod tests {
use super::*;
struct TestExt;
impl EtdlExtension for TestExt {
fn id(&self) -> &str {
"etdl.test"
}
fn version(&self) -> &str {
"1.0"
}
fn validate(
&self,
_doc: &EtlDocument,
_context: &ExtensionContext<'_>,
_diagnostics: &mut Vec<Diagnostic>,
) {
}
}
#[test]
fn registry_is_deterministic() {
let mut r = ExtensionRegistry::new();
r.register(TestExt);
r.register(TestExt);
assert!(r.contains("etdl.test"));
assert!(r.lookup("etdl.test").is_some());
assert_eq!(r.list(), vec!["etdl.test"]);
}
#[test]
fn lookup_missing_is_none() {
let r = ExtensionRegistry::new();
assert!(r.lookup("etdl.nope").is_none());
}
#[test]
fn every_built_in_extension_has_a_non_empty_descriptor() {
let registry = builtin_registry();
for id in registry.list() {
let ext = registry.lookup(id).expect("listed");
let d = ext.descriptor();
assert!(
!d.summary.is_empty(),
"{id}: EtdlExtension::descriptor() left `summary` at the trait default (empty) — implement it"
);
assert!(
!d.diagnostic_codes.is_empty(),
"{id}: EtdlExtension::descriptor() left `diagnostic_codes` at the trait default (empty) — implement it"
);
}
}
}