folk-plugin-grpc 0.2.2

gRPC plugin for Folk — unary call passthrough to PHP workers via tonic
Documentation
use folk_plugin_grpc::{GrpcConfig, GrpcEnvelope};

#[tokio::test]
async fn grpc_envelope_json_round_trip() {
    let env = GrpcEnvelope {
        service: "mypackage.MyService".into(),
        method: "SayHello".into(),
        payload: b"\x0a\x05world".to_vec(),
        metadata: Default::default(),
    };

    let json = serde_json::to_string(&env).unwrap();
    let decoded: GrpcEnvelope = serde_json::from_str(&json).unwrap();
    assert_eq!(decoded.service, env.service);
    assert_eq!(decoded.method, env.method);
    assert_eq!(decoded.payload, env.payload);
    assert!(decoded.metadata.is_empty());
}

#[tokio::test]
async fn grpc_envelope_payload_serializes_as_json_array() {
    let env = GrpcEnvelope {
        service: "test.Service".into(),
        method: "DoThing".into(),
        payload: vec![1, 2, 3],
        metadata: Default::default(),
    };

    let value: serde_json::Value = serde_json::to_value(&env).unwrap();
    let payload = value.get("payload").unwrap().as_array().unwrap();
    assert_eq!(payload.len(), 3);
    assert_eq!(payload[0], 1);
}

#[tokio::test]
async fn grpc_config_defaults() {
    let config: GrpcConfig = serde_json::from_str("{}").unwrap();
    assert_eq!(config.listen.to_string(), "0.0.0.0:50051");
    assert!(config.proto.is_empty());
    assert_eq!(config.max_recv_message_size, 4 * 1024 * 1024);
    assert_eq!(config.max_send_message_size, 4 * 1024 * 1024);
    assert!(config.timeout.is_none());
}

#[tokio::test]
async fn grpc_config_human_readable_sizes() {
    let json = r#"{
        "max_recv_message_size": "16mb",
        "max_send_message_size": "8mb"
    }"#;
    let config: GrpcConfig = serde_json::from_str(json).unwrap();
    assert_eq!(config.max_recv_message_size, 16 * 1024 * 1024);
    assert_eq!(config.max_send_message_size, 8 * 1024 * 1024);
}

#[tokio::test]
async fn grpc_config_numeric_sizes() {
    let json = r#"{
        "max_recv_message_size": 1024,
        "max_send_message_size": 2048
    }"#;
    let config: GrpcConfig = serde_json::from_str(json).unwrap();
    assert_eq!(config.max_recv_message_size, 1024);
    assert_eq!(config.max_send_message_size, 2048);
}

#[tokio::test]
async fn grpc_config_timeout() {
    let json = r#"{"timeout": "30s"}"#;
    let config: GrpcConfig = serde_json::from_str(json).unwrap();
    assert_eq!(config.timeout, Some(std::time::Duration::from_secs(30)));
}

#[tokio::test]
async fn grpc_config_defaults_p1_fields() {
    let config: GrpcConfig = serde_json::from_str("{}").unwrap();
    assert!(config.max_concurrent_streams.is_none());
    assert!(config.keepalive.is_none());
    assert!(config.tls.is_none());
}

#[tokio::test]
async fn grpc_config_keepalive() {
    let json = r#"{
        "keepalive": {
            "interval": "60s",
            "timeout": "20s"
        }
    }"#;
    let config: GrpcConfig = serde_json::from_str(json).unwrap();
    let ka = config.keepalive.unwrap();
    assert_eq!(ka.interval, std::time::Duration::from_secs(60));
    assert_eq!(ka.timeout, std::time::Duration::from_secs(20));
}

#[tokio::test]
async fn grpc_config_tls() {
    let json = r#"{
        "tls": {
            "cert": "/tmp/cert.pem",
            "key": "/tmp/key.pem"
        }
    }"#;
    let config: GrpcConfig = serde_json::from_str(json).unwrap();
    let tls = config.tls.unwrap();
    assert_eq!(tls.cert.to_str().unwrap(), "/tmp/cert.pem");
    assert_eq!(tls.key.to_str().unwrap(), "/tmp/key.pem");
}

#[tokio::test]
async fn grpc_config_max_concurrent_streams() {
    let json = r#"{"max_concurrent_streams": 200}"#;
    let config: GrpcConfig = serde_json::from_str(json).unwrap();
    assert_eq!(config.max_concurrent_streams, Some(200));
}

#[tokio::test]
async fn grpc_config_compression() {
    let config: GrpcConfig = serde_json::from_str("{}").unwrap();
    assert!(!config.compression);

    let config: GrpcConfig = serde_json::from_str(r#"{"compression": true}"#).unwrap();
    assert!(config.compression);
}

#[tokio::test]
async fn grpc_config_full_example() {
    let json = r#"{
        "listen": "0.0.0.0:9090",
        "proto": ["proto/service.proto"],
        "max_recv_message_size": "16mb",
        "max_send_message_size": "8mb",
        "timeout": "30s",
        "max_concurrent_streams": 200,
        "compression": true,
        "keepalive": {"interval": "60s", "timeout": "20s"},
        "tls": {"cert": "/etc/ssl/cert.pem", "key": "/etc/ssl/key.pem"}
    }"#;
    let config: GrpcConfig = serde_json::from_str(json).unwrap();
    assert_eq!(config.listen.to_string(), "0.0.0.0:9090");
    assert_eq!(config.proto, vec!["proto/service.proto"]);
    assert_eq!(config.max_recv_message_size, 16 * 1024 * 1024);
    assert_eq!(config.max_send_message_size, 8 * 1024 * 1024);
    assert_eq!(config.timeout, Some(std::time::Duration::from_secs(30)));
    assert_eq!(config.max_concurrent_streams, Some(200));
    assert!(config.compression);
    assert!(config.keepalive.is_some());
    assert!(config.tls.is_some());
}

#[tokio::test]
async fn gzip_round_trip() {
    use std::io::{Read as _, Write as _};

    let original = b"hello gRPC compression test data";

    // Compress
    let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
    encoder.write_all(original).unwrap();
    let compressed = encoder.finish().unwrap();

    // Decompress
    let mut decoder = flate2::read::GzDecoder::new(&compressed[..]);
    let mut decompressed = Vec::new();
    decoder.read_to_end(&mut decompressed).unwrap();

    assert_eq!(decompressed, original);
}

#[tokio::test]
#[ignore = "requires php + protobuf extension + grpcurl"]
async fn grpc_plugin_handles_unary_call() {
    // Start Folk with GrpcPlugin
    // Use grpcurl or a tonic client to call a method
    // Verify PHP side receives correct service/method/payload
    todo!()
}