jellyflow-runtime 0.2.0

Headless store, rules, schema, profile, and change pipeline for Jellyflow.
Documentation
use serde::{Deserialize, Serialize};

use jellyflow_core::core::Graph;

use super::ConformanceScenarioCompiled;
use super::action::ConformanceAction;
use super::behavior::ConformanceBehavior;
use super::constants::default_schema_version;
use super::setup::{ConformanceSetup, ConformanceTraceConfig};
use super::trace::ConformanceTraceEvent;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConformanceScenario {
    #[serde(default = "default_schema_version")]
    pub schema_version: u32,
    pub name: String,
    #[serde(default)]
    setup: ConformanceSetup,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub actions: Vec<ConformanceAction>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub behaviors: Vec<ConformanceBehavior>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub expected_trace: Vec<ConformanceTraceEvent>,
}

impl ConformanceScenario {
    pub fn new(name: impl Into<String>, graph: Graph) -> Self {
        Self {
            schema_version: default_schema_version(),
            name: name.into(),
            setup: ConformanceSetup::from_graph(graph),
            actions: Vec::new(),
            behaviors: Vec::new(),
            expected_trace: Vec::new(),
        }
    }

    pub fn with_view_state(mut self, view_state: crate::io::NodeGraphViewState) -> Self {
        self.setup.view_state = view_state;
        self
    }

    pub fn with_editor_config(mut self, editor_config: crate::io::NodeGraphEditorConfig) -> Self {
        self.setup.editor_config = editor_config;
        self
    }

    pub fn with_xyflow_callbacks(mut self) -> Self {
        self.setup.trace = ConformanceTraceConfig::with_xyflow_callbacks();
        self
    }

    pub fn with_actions(mut self, actions: impl IntoIterator<Item = ConformanceAction>) -> Self {
        self.actions = actions.into_iter().collect();
        self
    }

    pub fn with_behaviors(
        mut self,
        behaviors: impl IntoIterator<Item = ConformanceBehavior>,
    ) -> Self {
        self.behaviors = behaviors.into_iter().collect();
        self
    }

    pub fn with_behavior(mut self, behavior: ConformanceBehavior) -> Self {
        self.behaviors.push(behavior);
        self
    }

    pub fn with_expected_trace(
        mut self,
        expected_trace: impl IntoIterator<Item = ConformanceTraceEvent>,
    ) -> Self {
        self.expected_trace = expected_trace.into_iter().collect();
        self
    }

    pub(crate) fn compiled(&self) -> ConformanceScenarioCompiled {
        ConformanceScenarioCompiled::from_scenario(self)
    }

    pub(crate) fn setup(&self) -> &ConformanceSetup {
        &self.setup
    }

    pub fn expanded_actions(&self) -> Vec<ConformanceAction> {
        self.compiled().actions().to_vec()
    }

    pub fn expanded_expected_trace(&self) -> Vec<ConformanceTraceEvent> {
        self.compiled().expected_trace().to_vec()
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConformanceSuite {
    #[serde(default = "default_schema_version")]
    pub schema_version: u32,
    pub name: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub scenarios: Vec<ConformanceScenario>,
}

impl ConformanceSuite {
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            schema_version: default_schema_version(),
            name: name.into(),
            scenarios: Vec::new(),
        }
    }

    pub fn with_scenarios(
        mut self,
        scenarios: impl IntoIterator<Item = ConformanceScenario>,
    ) -> Self {
        self.scenarios = scenarios.into_iter().collect();
        self
    }

    pub fn push_scenario(&mut self, scenario: ConformanceScenario) {
        self.scenarios.push(scenario);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::runtime::conformance::ConformanceNodeDragSessionContract;
    use jellyflow_core::core::{CanvasPoint, Graph, GraphId, NodeId};

    #[test]
    fn compiled_scenario_strips_behavior_trace_suffix_from_approved_trace() {
        let node_id = NodeId::from_u128(1);
        let scenario = ConformanceScenario::new("compiled approval", Graph::new(GraphId::new()))
            .with_behavior(ConformanceBehavior::node_drag_session(
                ConformanceNodeDragSessionContract::new(
                    node_id,
                    CanvasPoint { x: 1.0, y: 2.0 },
                    CanvasPoint { x: 3.0, y: 4.0 },
                ),
            ));
        let compiled = scenario.compiled();
        let approved =
            compiled.approval_expected_trace(&scenario.expected_trace, compiled.expected_trace());

        assert!(scenario.expected_trace.is_empty());
        assert!(approved.is_empty());
        assert_eq!(
            compiled.expected_event_count(),
            compiled.expected_trace().len()
        );
        assert_eq!(compiled.actions().len(), 1);
    }
}