greentic-types 0.5.2

Shared primitives for Greentic: TenantCtx, InvocationEnvelope, NodeError, ids.
Documentation
#![cfg(feature = "serde")]

use greentic_types::pack::extensions::component_sources::{
    ArtifactLocationV1, ComponentSourceEntryV1, ComponentSourcesV1, ResolvedComponentV1,
    decode_component_sources_v1_from_cbor_bytes, encode_component_sources_v1_to_cbor_bytes,
};
use greentic_types::{
    ComponentId, ComponentSourceRef, ComponentSourceRefError, PackId, PackKind, PackManifest,
    PackSignatures, decode_pack_manifest, encode_pack_manifest,
};
use semver::Version;
use serde_json::json;

#[test]
fn component_source_ref_parses_and_formats() {
    let value = "oci://ghcr.io/acme/search@sha256:abc";
    let parsed: ComponentSourceRef = value.parse().expect("parse oci ref");
    assert_eq!(parsed.to_string(), value);
    assert_eq!(parsed.scheme(), "oci");
    assert_eq!(parsed.reference(), "ghcr.io/acme/search@sha256:abc");

    assert_eq!(
        "store://vendor/search@1.2.3"
            .parse::<ComponentSourceRef>()
            .expect("parse store ref")
            .scheme(),
        "store"
    );
    let file_value = "file:///var/components/search.wasm";
    let parsed_file: ComponentSourceRef = file_value.parse().expect("parse file ref");
    assert_eq!(parsed_file.to_string(), file_value);
    assert_eq!(parsed_file.scheme(), "file");
    assert_eq!(parsed_file.reference(), "/var/components/search.wasm");

    assert_eq!(
        "oci://".parse::<ComponentSourceRef>().unwrap_err(),
        ComponentSourceRefError::MissingLocator
    );
    assert_eq!(
        "http://example.com"
            .parse::<ComponentSourceRef>()
            .unwrap_err(),
        ComponentSourceRefError::InvalidScheme
    );
    assert_eq!(
        "oci://bad ref".parse::<ComponentSourceRef>().unwrap_err(),
        ComponentSourceRefError::ContainsWhitespace
    );
}

#[test]
fn component_source_ref_detects_tag_and_digest() {
    let tagged: ComponentSourceRef = "oci://ghcr.io/acme/search:1.2.3".parse().unwrap();
    assert!(tagged.is_tag());
    assert!(!tagged.is_digest());
    assert_eq!(tagged.normalized(), "oci://ghcr.io/acme/search:1.2.3");

    let digested: ComponentSourceRef = "oci://ghcr.io/acme/search@sha256:abc".parse().unwrap();
    assert!(digested.is_digest());
    assert!(!digested.is_tag());
    assert_eq!(
        digested.normalized(),
        "oci://ghcr.io/acme/search@sha256:abc"
    );

    let with_port: ComponentSourceRef = "oci://registry.local:5000/acme/search:latest"
        .parse()
        .unwrap();
    assert!(with_port.is_tag());
    assert!(!with_port.is_digest());

    let port_without_tag: ComponentSourceRef =
        "oci://registry.local:5000/acme/search".parse().unwrap();
    assert!(!port_without_tag.is_tag());
    assert!(!port_without_tag.is_digest());

    let tagged_and_digested: ComponentSourceRef =
        "oci://registry.local:5000/acme/search:latest@sha256:abc"
            .parse()
            .unwrap();
    assert!(tagged_and_digested.is_digest());
    assert!(!tagged_and_digested.is_tag());
    assert_eq!(
        tagged_and_digested.normalized(),
        "oci://registry.local:5000/acme/search@sha256:abc"
    );

    let repo_ref: ComponentSourceRef = "repo://acme/search@1.2.3".parse().unwrap();
    assert!(!repo_ref.is_tag());
    assert!(!repo_ref.is_digest());
    assert_eq!(repo_ref.normalized(), "repo://acme/search@1.2.3");
}

#[test]
fn component_sources_payload_roundtrips_and_matches_shape() {
    let sources = ComponentSourcesV1::new(vec![
        ComponentSourceEntryV1 {
            name: "search".into(),
            component_id: Some(ComponentId::new("vendor.search").unwrap()),
            source: "oci://ghcr.io/acme/search@sha256:abc".parse().unwrap(),
            resolved: ResolvedComponentV1 {
                digest: "sha256:abc".into(),
                signature: Some("sig:ed25519:deadbeef".into()),
                signed_by: Some("acme".into()),
            },
            artifact: ArtifactLocationV1::Inline {
                wasm_path: "components/search.wasm".into(),
                manifest_path: Some("components/search.manifest.cbor".into()),
            },
            licensing_hint: None,
            metering_hint: None,
        },
        ComponentSourceEntryV1 {
            name: "cache".into(),
            component_id: None,
            source: "store://vendor/cache@1.2.0".parse().unwrap(),
            resolved: ResolvedComponentV1 {
                digest: "sha256:def".into(),
                signature: None,
                signed_by: None,
            },
            artifact: ArtifactLocationV1::Remote,
            licensing_hint: Some("license:pro".into()),
            metering_hint: Some("metered:requests".into()),
        },
    ]);

    let value = sources.to_extension_value().expect("to extension value");
    let expected = json!({
        "schema_version": 1,
        "components": [
            {
                "name": "search",
                "component_id": "vendor.search",
                "source": "oci://ghcr.io/acme/search@sha256:abc",
                "resolved": {
                    "digest": "sha256:abc",
                    "signature": "sig:ed25519:deadbeef",
                    "signed_by": "acme"
                },
                "artifact": {
                    "inline": {
                        "wasm_path": "components/search.wasm",
                        "manifest_path": "components/search.manifest.cbor"
                    }
                }
            },
            {
                "name": "cache",
                "source": "store://vendor/cache@1.2.0",
                "resolved": {
                    "digest": "sha256:def"
                },
                "artifact": "remote",
                "licensing_hint": "license:pro",
                "metering_hint": "metered:requests"
            }
        ]
    });
    assert_eq!(value, expected);

    let parsed = ComponentSourcesV1::from_extension_value(&value).expect("from extension value");
    assert_eq!(parsed, sources);

    let cbor = encode_component_sources_v1_to_cbor_bytes(&sources).expect("encode cbor");
    let decoded =
        decode_component_sources_v1_from_cbor_bytes(&cbor).expect("decode component sources");
    assert_eq!(decoded, sources);
}

#[test]
fn pack_manifest_component_sources_helpers_work() {
    let mut manifest = PackManifest {
        schema_version: "pack-v1".into(),
        pack_id: PackId::new("vendor.pack").unwrap(),
        name: None,
        version: Version::parse("1.2.0").unwrap(),
        kind: PackKind::Application,
        publisher: "vendor".into(),
        components: Vec::new(),
        flows: Vec::new(),
        dependencies: Vec::new(),
        capabilities: Vec::new(),
        secret_requirements: Vec::new(),
        signatures: PackSignatures::default(),
        bootstrap: None,
        extensions: None,
    };

    let sources = ComponentSourcesV1::new(vec![ComponentSourceEntryV1 {
        name: "search".into(),
        component_id: Some(ComponentId::new("vendor.search").unwrap()),
        source: "oci://ghcr.io/acme/search@sha256:abc".parse().unwrap(),
        resolved: ResolvedComponentV1 {
            digest: "sha256:abc".into(),
            signature: None,
            signed_by: None,
        },
        artifact: ArtifactLocationV1::Remote,
        licensing_hint: None,
        metering_hint: None,
    }]);

    manifest
        .set_component_sources_v1(sources.clone())
        .expect("set component sources");
    let roundtrip = manifest
        .get_component_sources_v1()
        .expect("get component sources");
    assert_eq!(roundtrip, Some(sources));
}

#[test]
fn pack_manifest_without_component_sources_still_decodes() {
    let manifest = PackManifest {
        schema_version: "pack-v1".into(),
        pack_id: PackId::new("vendor.pack").unwrap(),
        name: None,
        version: Version::parse("1.0.0").unwrap(),
        kind: PackKind::Application,
        publisher: "vendor".into(),
        components: Vec::new(),
        flows: Vec::new(),
        dependencies: Vec::new(),
        capabilities: Vec::new(),
        secret_requirements: Vec::new(),
        signatures: PackSignatures::default(),
        bootstrap: None,
        extensions: None,
    };

    let cbor = encode_pack_manifest(&manifest).expect("encode pack manifest");
    let decoded = decode_pack_manifest(&cbor).expect("decode pack manifest");
    assert!(decoded.extensions.is_none());
}