switchback-protobuf 0.0.1-0.dev.5

Protobuf parser for the switchback framework.
mod common;

use common::{
    assert_sources_match_inputs, codec_roundtrip, load_examples, load_loose_fixture,
    mirrored_compilers, normalize, rebuild_buf_module, restore_sources_map, run_buf_lint_format,
};
use switchback_protobuf::examples::{EXAMPLE_PROTO_INPUTS, fixtures_proto_dir};
use switchback_protocols::{DecodedAttachment, GrpcPayloadKind, ProtocolRegistry};
use switchback_traits::EntityBody;
use tempfile::tempdir;

#[test]
fn mirrored_compile_parity_examples_corpus() {
    let compilers = mirrored_compilers();
    assert!(
        compilers.len() >= 2,
        "expected protoc and buf with default features"
    );

    let mut manuals = Vec::new();
    for compiler in compilers {
        manuals.push(normalize(load_examples(compiler)));
    }
    for manual in &manuals[1..] {
        assert_eq!(&manuals[0], manual, "compiler parity on examples corpus");
    }
}

#[test]
fn mirrored_compile_parity_doc_rich_fixture() {
    let compilers = mirrored_compilers();
    let mut manuals = Vec::new();
    for compiler in compilers {
        manuals.push(normalize(load_loose_fixture("doc_rich.proto", compiler)));
    }
    for manual in &manuals[1..] {
        assert_eq!(&manuals[0], manual, "compiler parity on doc_rich.proto");
    }
}

#[test]
fn codec_roundtrip_and_directory_faithful_proto_restore() {
    let fixture_proto_dir = fixtures_proto_dir();
    for compiler in mirrored_compilers() {
        let manual = normalize(load_examples(compiler));
        assert_sources_match_inputs(&manual, &fixture_proto_dir, EXAMPLE_PROTO_INPUTS);

        let restored = normalize(codec_roundtrip(&manual));
        assert_eq!(manual, restored, "codec round-trip ({compiler:?})");

        let temp = tempdir().expect("tempdir");
        let module_root = temp.path();
        restore_sources_map(&restored, module_root);
        for input in EXAMPLE_PROTO_INPUTS {
            let expected = std::fs::read(fixture_proto_dir.join(input))
                .unwrap_or_else(|e| panic!("read fixture {input}: {e}"));
            let got = std::fs::read(module_root.join(input))
                .unwrap_or_else(|e| panic!("read restored {input}: {e}"));
            assert_eq!(
                expected, got,
                "restored proto bytes for {input} ({compiler:?})"
            );
        }

        let rebuilt = tempdir().expect("rebuilt tempdir");
        rebuild_buf_module(&restored, &fixture_proto_dir, rebuilt.path())
            .expect("rebuild buf module");
        run_buf_lint_format(rebuilt.path());

        let reloaded_from_rebuilt = {
            let export = common::ensure_proto_deps_export();
            let args = switchback_protobuf::LoadArgs {
                compiler,
                module_root: rebuilt.path().to_path_buf(),
                inputs: EXAMPLE_PROTO_INPUTS
                    .iter()
                    .map(|p| std::path::PathBuf::from(*p))
                    .collect(),
                proto_paths: vec![rebuilt.path().to_path_buf(), export.clone()],
                protoc_path: None,
                buf_path: None,
                proto_deps_export: Some(export),
                title: None,
            };
            normalize(
                switchback_protobuf::load(&args)
                    .unwrap_or_else(|e| panic!("reload rebuilt module ({compiler:?}): {e}")),
            )
        };
        assert_eq!(
            manual, reloaded_from_rebuilt,
            "parse → wire → parse idempotency ({compiler:?})"
        );
    }
}

#[test]
fn structural_smoke_examples_corpus() {
    let manual = load_examples(mirrored_compilers()[0]);
    let contract = &manual.modules[0].contracts[0];

    assert!(!contract.groups.is_empty());
    assert_eq!(manual.sources.len(), EXAMPLE_PROTO_INPUTS.len());

    let mut categories = std::collections::BTreeSet::new();
    let mut entity_count = 0usize;
    for group in &contract.groups {
        for entity in &group.entities {
            categories.insert(entity.category.as_str());
            entity_count += 1;
        }
    }
    assert!(entity_count > 0);
    assert!(categories.contains("schema"));
    assert!(categories.contains("service"));
    assert!(categories.contains("operation"));
    assert!(
        !contract.companions.is_empty(),
        "expected README companions from examples tree"
    );

    for input in EXAMPLE_PROTO_INPUTS {
        let doc = manual
            .sources
            .iter()
            .find(|d| d.source_ref.uri == *input)
            .expect("source document");
        assert!(
            doc.source_ref.uri.contains('/'),
            "uri must preserve directories: {}",
            doc.source_ref.uri
        );
    }
}

#[test]
fn grpc_metadata_parameters_from_method_options() {
    let manual = load_examples(mirrored_compilers()[0]);
    let contract = &manual.modules[0].contracts[0];
    let registry = ProtocolRegistry::with_builtins();

    let echo_unary = contract
        .groups
        .iter()
        .find(|g| g.id.as_str() == "acme.example.v1")
        .and_then(|g| {
            g.entities
                .iter()
                .find(|e| e.name == "EchoService.EchoUnary")
        })
        .expect("EchoUnary operation");
    let EntityBody::Operation(body) = &echo_unary.body else {
        panic!("expected operation body");
    };
    let request_id = body
        .parameters
        .iter()
        .find(|p| p.name == "x-request-id")
        .expect("x-request-id metadata");
    assert_eq!(request_id.location, "metadata");
    assert!(request_id.required);
    assert_eq!(request_id.protocols.len(), 1);
    assert_eq!(request_id.protocols[0].protocol_id, "grpc");
    let decoded = registry
        .decode_attachment(&request_id.protocols[0])
        .expect("decode metadata attachment");
    let DecodedAttachment::Grpc(GrpcPayloadKind::Metadata(meta)) = decoded else {
        panic!("expected GrpcMetadataMeta payload");
    };
    assert_eq!(meta.key, "x-request-id");
    assert!(meta.required);

    let relay = contract
        .groups
        .iter()
        .find(|g| g.id.as_str() == "acme.example.v1")
        .and_then(|g| {
            g.entities
                .iter()
                .find(|e| e.name == "GatewayService.RelayConnect")
        })
        .expect("RelayConnect operation");
    let EntityBody::Operation(relay_body) = &relay.body else {
        panic!("expected operation body");
    };
    assert!(
        relay_body
            .parameters
            .iter()
            .any(|p| p.name == "x-session-token" && p.location == "metadata" && p.required)
    );
}

#[test]
fn wire_policy_serialize_succeeds_without_unresolved_links() {
    let manual = load_examples(mirrored_compilers()[0]);
    let _ = codec_roundtrip(&manual);
}