greentic-types 0.5.2

Shared primitives for Greentic: TenantCtx, InvocationEnvelope, NodeError, ids.
Documentation
use std::collections::BTreeMap;

use greentic_types::{
    ComponentCapabilities, ComponentManifest, ComponentOperation, ComponentProfiles, Flow,
    FlowComponentRef, FlowId, FlowKind, FlowMetadata, InputMapping, Node, OutputMapping,
    ResourceHints, Routing, TelemetryHints,
};
use indexmap::IndexMap;
use semver::Version;
use serde_json::Value;

#[test]
fn flow_ingress_respects_insertion_order() {
    let node_a_id: FlowId = "flow.demo".parse().unwrap();
    let mut nodes: IndexMap<_, _, greentic_types::flow::FlowHasher> = IndexMap::default();
    nodes.insert(
        "first".parse().unwrap(),
        Node {
            id: "first".parse().unwrap(),
            component: component_ref("component.first"),
            input: InputMapping {
                mapping: Value::Null,
            },
            output: OutputMapping {
                mapping: Value::Null,
            },
            err_map: None,
            routing: Routing::Next {
                node_id: "second".parse().unwrap(),
            },
            telemetry: TelemetryHints::default(),
        },
    );
    nodes.insert(
        "second".parse().unwrap(),
        Node {
            id: "second".parse().unwrap(),
            component: component_ref("component.second"),
            input: InputMapping {
                mapping: Value::Null,
            },
            output: OutputMapping {
                mapping: Value::Null,
            },
            err_map: None,
            routing: Routing::End,
            telemetry: TelemetryHints::default(),
        },
    );

    let flow = Flow {
        schema_version: "flow-v1".into(),
        id: node_a_id,
        kind: FlowKind::Messaging,
        entrypoints: BTreeMap::new(),
        nodes,
        metadata: FlowMetadata::default(),
    };

    let ingress = flow.ingress().expect("ingress");
    assert_eq!(ingress.0.as_str(), "first");
}

#[test]
fn flow_json_roundtrips_with_routing_variants() {
    let mut nodes: IndexMap<_, _, greentic_types::flow::FlowHasher> = IndexMap::default();
    nodes.insert(
        "branch".parse().unwrap(),
        Node {
            id: "branch".parse().unwrap(),
            component: component_ref("component.branch"),
            input: InputMapping {
                mapping: serde_json::json!({"input": "value"}),
            },
            output: OutputMapping {
                mapping: serde_json::json!({"output": "value"}),
            },
            err_map: None,
            routing: Routing::Branch {
                on_status: BTreeMap::from([("ok".to_string(), "next".parse().unwrap())]),
                default: Some("end".parse().unwrap()),
            },
            telemetry: TelemetryHints::default(),
        },
    );
    nodes.insert(
        "next".parse().unwrap(),
        Node {
            id: "next".parse().unwrap(),
            component: component_ref("component.next"),
            input: InputMapping {
                mapping: Value::Null,
            },
            output: OutputMapping {
                mapping: Value::Null,
            },
            err_map: None,
            routing: Routing::Reply,
            telemetry: TelemetryHints::default(),
        },
    );
    nodes.insert(
        "end".parse().unwrap(),
        Node {
            id: "end".parse().unwrap(),
            component: component_ref("component.end"),
            input: InputMapping {
                mapping: Value::Null,
            },
            output: OutputMapping {
                mapping: Value::Null,
            },
            err_map: None,
            routing: Routing::End,
            telemetry: TelemetryHints::default(),
        },
    );

    let flow = Flow {
        schema_version: "flow-v1".into(),
        id: "flow.branching".parse().unwrap(),
        kind: FlowKind::Job,
        entrypoints: BTreeMap::from([("default".into(), Value::Null)]),
        nodes,
        metadata: FlowMetadata::default(),
    };

    let encoded = serde_json::to_string(&flow).expect("serialize");
    let decoded: Flow = serde_json::from_str(&encoded).expect("deserialize");
    assert_eq!(decoded, flow);
}

#[test]
fn component_manifest_defaults_extend() {
    let manifest = ComponentManifest {
        id: "component.profile".parse().unwrap(),
        version: Version::parse("1.0.0").unwrap(),
        supports: vec![FlowKind::Messaging],
        world: "test:component@1.0.0".into(),
        profiles: ComponentProfiles {
            default: Some("default".into()),
            supported: vec!["default".into(), "advanced".into()],
        },
        capabilities: ComponentCapabilities::default(),
        configurators: None,
        operations: vec![ComponentOperation {
            name: "handle".into(),
            input_schema: Value::Null,
            output_schema: Value::Null,
        }],
        config_schema: None,
        resources: ResourceHints::default(),
        dev_flows: BTreeMap::new(),
    };

    let default = manifest.select_profile(None).expect("default");
    assert_eq!(default, Some("default"));

    let explicit = manifest.select_profile(Some("advanced")).expect("advanced");
    assert_eq!(explicit, Some("advanced"));

    let err = manifest
        .select_profile(Some("unknown"))
        .expect_err("should fail");
    assert!(matches!(
        err,
        greentic_types::ComponentProfileError::UnsupportedProfile { .. }
    ));
}

fn component_ref(id: &str) -> FlowComponentRef {
    FlowComponentRef {
        id: id.parse().unwrap(),
        pack_alias: None,
        operation: None,
    }
}