bijux-dag-runtime 0.4.1

Execution engine, replay semantics, and runtime policy layer for Bijux DAG graphs.
Documentation
use crate::{NodeResult, RunContext, RuntimeError};
use bijux_dag_core::{Effect, Graph, Node};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AdapterOrigin {
    BuiltIn,
    External,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CacheCompatibilityMode {
    FingerprintExact,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdapterId {
    pub id: String,
    pub version: String,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct EffectSet {
    pub filesystem: bool,
    pub env: bool,
    pub network: bool,
    pub clock: bool,
}

impl EffectSet {
    pub fn from_effects(effects: &[Effect]) -> Self {
        let mut set = EffectSet::default();
        for e in effects {
            match e {
                Effect::Filesystem => set.filesystem = true,
                Effect::Env => set.env = true,
                Effect::Network => set.network = true,
                Effect::Clock => set.clock = true,
            }
        }
        set
    }
}

pub struct NodeCtx<'a> {
    pub graph: &'a Graph,
    pub node: &'a Node,
    pub exec: &'a RunContext,
    pub params: &'a serde_json::Value,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AdapterDescriptor {
    pub id: String,
    pub version: String,
    pub supported_kinds: Vec<String>,
    pub required_effects: EffectSet,
    pub produces_outputs_schema_version: String,
    pub origin: AdapterOrigin,
    pub protocol_version: String,
    pub cache_compatibility: CacheCompatibilityMode,
    pub supports_timeout: bool,
    pub supports_cancel: bool,
    pub binary_hash: Option<String>,
}

pub trait Adapter: Send + Sync {
    fn id(&self) -> AdapterId;
    fn supported_kinds(&self) -> Vec<String>;
    fn required_effects(&self) -> EffectSet;
    fn produces_outputs_schema_version(&self) -> String;
    fn protocol_version(&self) -> String {
        "bijux-dag-adapter/v1".to_string()
    }
    fn cache_compatibility(&self) -> CacheCompatibilityMode {
        CacheCompatibilityMode::FingerprintExact
    }
    fn supports_timeout(&self) -> bool {
        true
    }
    fn supports_cancel(&self) -> bool {
        false
    }
    fn origin(&self) -> AdapterOrigin {
        AdapterOrigin::BuiltIn
    }
    fn descriptor(&self) -> AdapterDescriptor {
        let id = self.id();
        AdapterDescriptor {
            id: id.id,
            version: id.version,
            supported_kinds: self.supported_kinds(),
            required_effects: self.required_effects(),
            produces_outputs_schema_version: self.produces_outputs_schema_version(),
            origin: self.origin(),
            protocol_version: self.protocol_version(),
            cache_compatibility: self.cache_compatibility(),
            supports_timeout: self.supports_timeout(),
            supports_cancel: self.supports_cancel(),
            binary_hash: self.binary_hash(),
        }
    }
    fn binary_hash(&self) -> Option<String> {
        None
    }
    fn execute(&self, ctx: &NodeCtx) -> Result<NodeResult, RuntimeError>;
}

#[cfg(test)]
mod tests {
    use super::*;

    struct NoopAdapter;

    impl Adapter for NoopAdapter {
        fn id(&self) -> AdapterId {
            AdapterId { id: "noop".to_string(), version: "0.1".to_string() }
        }

        fn supported_kinds(&self) -> Vec<String> {
            vec!["const".to_string()]
        }

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

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

        fn origin(&self) -> AdapterOrigin {
            AdapterOrigin::External
        }

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

    #[test]
    fn effect_set_maps_all_effects() {
        let set = EffectSet::from_effects(&[
            Effect::Filesystem,
            Effect::Env,
            Effect::Network,
            Effect::Clock,
        ]);
        assert!(set.filesystem);
        assert!(set.env);
        assert!(set.network);
        assert!(set.clock);
    }

    #[test]
    fn descriptor_contains_identity_origin_and_schema() {
        let adapter = NoopAdapter;
        let descriptor = adapter.descriptor();
        assert_eq!(descriptor.id, "noop");
        assert_eq!(descriptor.version, "0.1");
        assert_eq!(descriptor.supported_kinds, vec!["const".to_string()]);
        assert_eq!(descriptor.produces_outputs_schema_version, "v0.1");
        assert_eq!(descriptor.origin, AdapterOrigin::External);
        assert_eq!(descriptor.protocol_version, "bijux-dag-adapter/v1");
        assert_eq!(descriptor.cache_compatibility, CacheCompatibilityMode::FingerprintExact);
        assert!(descriptor.supports_timeout);
        assert!(!descriptor.supports_cancel);
    }
}