actrpc-transport 0.1.0

Transport abstractions and implementations for ActRPC.
Documentation
use actrpc_transport::{
    client::StdioJsonRpcClient,
    target::{StdioTarget, TransportTarget},
};
use serde_json::json;

#[cfg(unix)]
#[tokio::test]
async fn test_stdio_client_roundtrip_newline_delimited_with_cat() {
    let target = stdio_target("cat", "newline_delimited");
    let client = StdioJsonRpcClient::new(target).unwrap();

    let request = request(11, "echo");
    let response = actrpc_transport::JsonRpcClient::send(&client, request.clone())
        .await
        .unwrap();

    assert_eq!(response, request);
}

#[cfg(unix)]
#[tokio::test]
async fn test_stdio_client_roundtrip_content_length_with_cat() {
    let target = stdio_target("cat", "content_length");
    let client = StdioJsonRpcClient::new(target).unwrap();

    let request = request(12, "echo");
    let response = actrpc_transport::JsonRpcClient::send(&client, request.clone())
        .await
        .unwrap();

    assert_eq!(response, request);
}

#[cfg(windows)]
#[tokio::test]
async fn test_stdio_client_roundtrip_newline_delimited_with_powershell() {
    let target = windows_echo_stdio_target("newline_delimited");
    let client = StdioJsonRpcClient::new(target).unwrap();

    let request = request(21, "echo");
    let response = actrpc_transport::JsonRpcClient::send(&client, request.clone())
        .await
        .unwrap();

    assert_eq!(response, request);
}

#[cfg(windows)]
#[tokio::test]
async fn test_stdio_client_roundtrip_content_length_with_powershell() {
    let target = windows_echo_stdio_target("content_length");
    let client = StdioJsonRpcClient::new(target).unwrap();

    let request = request(22, "echo");
    let response = actrpc_transport::JsonRpcClient::send(&client, request.clone())
        .await
        .unwrap();

    assert_eq!(response, request);
}

#[test]
fn test_stdio_client_spawn_failure_returns_error() {
    let target = stdio_target("__actrpc_missing_stdio_program__", "newline_delimited");

    let err = StdioJsonRpcClient::new(target).unwrap_err();

    assert!(err.to_string().contains("failed to spawn stdio target"));
}

fn stdio_target(program: &str, framing: &str) -> StdioTarget {
    let target: TransportTarget = serde_json::from_value(json!({
        "stdio": {
            "program": program,
            "args": [],
            "env": [],
            "framing": framing
        }
    }))
    .unwrap();

    let TransportTarget::Stdio(target) = target else {
        panic!("expected stdio target");
    };

    target
}

#[cfg(any(unix, windows))]
fn request(id: u64, method: impl Into<String>) -> actrpc_core::json_rpc::JsonRpcMessage {
    use actrpc_core::json_rpc::{
        JsonRpcId, JsonRpcMessage, JsonRpcParams, JsonRpcRequest, JsonRpcSingleMessage,
        JsonRpcVersion,
    };

    JsonRpcMessage::Single(JsonRpcSingleMessage::Request(JsonRpcRequest {
        jsonrpc: JsonRpcVersion::V2_0,
        id: JsonRpcId::Number(id.into()),
        method: method.into(),
        params: Some(JsonRpcParams::Array(vec![json!(1), json!(2)])),
    }))
}

#[cfg(windows)]
fn windows_echo_stdio_target(framing: &str) -> StdioTarget {
    let script = r#"
$stdin = [Console]::OpenStandardInput()
$stdout = [Console]::OpenStandardOutput()
$buffer = New-Object byte[] 8192

while (($read = $stdin.Read($buffer, 0, $buffer.Length)) -gt 0) {
    $stdout.Write($buffer, 0, $read)
    $stdout.Flush()
}
"#;

    let target: TransportTarget = serde_json::from_value(json!({
        "stdio": {
            "program": "powershell.exe",
            "args": [
                "-NoProfile",
                "-NonInteractive",
                "-Command",
                script
            ],
            "env": [],
            "framing": framing
        }
    }))
    .unwrap();

    let TransportTarget::Stdio(target) = target else {
        panic!("expected stdio target");
    };

    target
}