bijux-dag-runtime 0.4.1

Execution engine, replay semantics, and runtime policy layer for Bijux DAG graphs.
Documentation
use crate::adapter::{Adapter, AdapterDescriptor};
use crate::external_adapter;
use crate::{AdapterInfo, RuntimeError};
use std::collections::{BTreeSet, HashMap};
use std::sync::Arc;

pub struct AdapterRegistry {
    by_kind: HashMap<String, Arc<dyn Adapter>>,
}

impl AdapterRegistry {
    pub fn new() -> Self {
        Self { by_kind: HashMap::new() }
    }

    pub fn register_adapter(&mut self, adapter: Arc<dyn Adapter>) -> Result<(), RuntimeError> {
        for kind in adapter.supported_kinds() {
            self.register(kind, Arc::clone(&adapter))?;
        }
        Ok(())
    }

    pub fn register(
        &mut self,
        kind: String,
        adapter: Arc<dyn Adapter>,
    ) -> Result<(), RuntimeError> {
        if kind.trim().is_empty() {
            return Err(RuntimeError::Executor("adapter kind must not be empty".to_string()));
        }
        if self.by_kind.contains_key(&kind) {
            return Err(RuntimeError::Executor(format!(
                "multiple adapters registered for kind {}",
                kind
            )));
        }
        self.by_kind.insert(kind, adapter);
        Ok(())
    }

    pub fn resolve(&self, kind: &str) -> Result<Arc<dyn Adapter>, RuntimeError> {
        self.by_kind
            .get(kind)
            .map(Arc::clone)
            .ok_or_else(|| RuntimeError::Executor("missing adapter".to_string()))
    }

    pub fn list(&self) -> Vec<AdapterInfo> {
        let mut list = Vec::new();
        for adapter in self.by_kind.values() {
            let id = adapter.id();
            let req = adapter.required_effects();
            let mut effects = Vec::new();
            if req.filesystem {
                effects.push("filesystem".to_string());
            }
            if req.env {
                effects.push("env".to_string());
            }
            if req.network {
                effects.push("network".to_string());
            }
            if req.clock {
                effects.push("clock".to_string());
            }
            list.push(AdapterInfo { adapter_id: id.id, adapter_version: id.version, effects });
        }
        list.sort_by(|a, b| a.adapter_id.cmp(&b.adapter_id));
        list
    }

    pub fn descriptors(&self) -> Vec<AdapterDescriptor> {
        let mut seen = BTreeSet::new();
        let mut descriptors = Vec::new();
        for adapter in self.by_kind.values() {
            let descriptor = adapter.descriptor();
            let key = format!("{}@{}", descriptor.id, descriptor.version);
            if seen.insert(key) {
                descriptors.push(descriptor);
            }
        }
        descriptors
            .sort_by(|left, right| left.id.cmp(&right.id).then(left.version.cmp(&right.version)));
        descriptors
    }
}

pub fn build_registry(builtins: Vec<Arc<dyn Adapter>>) -> Result<AdapterRegistry, RuntimeError> {
    let mut registry = AdapterRegistry::new();
    for adapter in builtins {
        registry.register_adapter(adapter)?;
    }
    if let Ok(external) = external_adapter::discover_external_adapters() {
        for adapter in external {
            registry.register_adapter(adapter)?;
        }
    }
    Ok(registry)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{AdapterId, EffectSet, NodeCtx, NodeResult};
    use std::sync::Arc;

    struct DummyAdapter {
        id: &'static str,
        version: &'static str,
        kinds: Vec<String>,
    }

    impl Adapter for DummyAdapter {
        fn id(&self) -> AdapterId {
            AdapterId { id: self.id.to_string(), version: self.version.to_string() }
        }

        fn supported_kinds(&self) -> Vec<String> {
            self.kinds.clone()
        }

        fn required_effects(&self) -> EffectSet {
            EffectSet::default()
        }

        fn produces_outputs_schema_version(&self) -> String {
            "v0.1".to_string()
        }

        fn execute(&self, _ctx: &NodeCtx) -> Result<NodeResult, RuntimeError> {
            Err(RuntimeError::Executor("not used".to_string()))
        }
    }

    #[test]
    fn duplicate_kind_registration_is_rejected() {
        let mut registry = AdapterRegistry::new();
        registry
            .register_adapter(Arc::new(DummyAdapter {
                id: "a",
                version: "0.1",
                kinds: vec!["const".to_string()],
            }))
            .expect("first adapter registers");
        let err = registry
            .register_adapter(Arc::new(DummyAdapter {
                id: "b",
                version: "0.1",
                kinds: vec!["const".to_string()],
            }))
            .expect_err("duplicate kind should fail");
        assert!(format!("{err}").contains("multiple adapters registered for kind const"));
    }

    #[test]
    fn empty_kind_registration_is_rejected() {
        let mut registry = AdapterRegistry::new();
        let err = registry
            .register_adapter(Arc::new(DummyAdapter {
                id: "a",
                version: "0.1",
                kinds: vec!["".to_string()],
            }))
            .expect_err("empty kind should fail");
        assert!(format!("{err}").contains("adapter kind must not be empty"));
    }

    #[test]
    fn list_order_is_deterministic_by_adapter_id() {
        let mut registry = AdapterRegistry::new();
        registry
            .register_adapter(Arc::new(DummyAdapter {
                id: "z-adapter",
                version: "0.1",
                kinds: vec!["kind-z".to_string()],
            }))
            .expect("register z adapter");
        registry
            .register_adapter(Arc::new(DummyAdapter {
                id: "a-adapter",
                version: "0.1",
                kinds: vec!["kind-a".to_string()],
            }))
            .expect("register a adapter");
        let listed = registry.list();
        let ids: Vec<_> = listed.into_iter().map(|row| row.adapter_id).collect();
        assert_eq!(ids, vec!["a-adapter".to_string(), "z-adapter".to_string()]);
    }
}