tripley-rpc-cli 0.1.0

User-facing command line tool for Tripley RPC IDL and code generation.
use std::process::Command;

use camino::{Utf8Path, Utf8PathBuf};
use tempfile::TempDir;

#[test]
fn validate_and_emit_ir_work() {
    let temp = TempDir::new().expect("tempdir");
    let ir = temp_path(&temp, "demo.ir.json");

    run_tripley_rpc(&[
        "validate",
        "--in",
        kiosk_idl().as_str(),
        "--import-root",
        kiosk_root().as_str(),
    ]);
    run_tripley_rpc(&[
        "emit-ir",
        "--in",
        kiosk_idl().as_str(),
        "--import-root",
        kiosk_root().as_str(),
        "--out",
        ir.as_str(),
    ]);

    let text = std::fs::read_to_string(ir).expect("read ir");
    assert!(text.contains("\"package\": \"kiosk.card\""));
    assert!(text.contains("\"name\": \"CardReader\""));
}

#[test]
fn compat_accepts_same_idl() {
    let output = run_tripley_rpc(&[
        "compat",
        "--old",
        kiosk_idl().as_str(),
        "--old-import-root",
        kiosk_root().as_str(),
        "--new",
        kiosk_idl().as_str(),
        "--new-import-root",
        kiosk_root().as_str(),
    ]);

    assert!(String::from_utf8_lossy(&output.stdout).contains("unchanged"));
}

#[test]
fn generate_rust_writes_sdk_files_directly_to_output_dir() {
    let temp = TempDir::new().expect("tempdir");
    let out = temp_path(&temp, "sdk");

    run_tripley_rpc(&[
        "generate",
        "rust",
        "--in",
        kiosk_idl().as_str(),
        "--import-root",
        kiosk_root().as_str(),
        "--out",
        out.as_str(),
        "--crate",
        "kiosk_sdk",
    ]);

    for file in [
        "activation.rs",
        "client.rs",
        "mod.rs",
        "server.rs",
        "services.rs",
        "types.rs",
    ] {
        assert!(out.join(file).exists(), "missing generated file {file}");
    }
}

#[test]
fn generate_rust_can_write_client_or_server_side_only() {
    let temp = TempDir::new().expect("tempdir");
    let client_out = temp_path(&temp, "client-sdk");
    let server_out = temp_path(&temp, "server-sdk");

    run_tripley_rpc(&[
        "generate",
        "rust",
        "--in",
        kiosk_idl().as_str(),
        "--import-root",
        kiosk_root().as_str(),
        "--out",
        client_out.as_str(),
        "--side",
        "client",
    ]);
    assert!(client_out.join("client.rs").exists());
    assert!(client_out.join("mod.rs").exists());
    assert!(client_out.join("services.rs").exists());
    assert!(client_out.join("types.rs").exists());
    assert!(!client_out.join("activation.rs").exists());
    assert!(!client_out.join("server.rs").exists());

    run_tripley_rpc(&[
        "generate",
        "rust",
        "--in",
        kiosk_idl().as_str(),
        "--import-root",
        kiosk_root().as_str(),
        "--out",
        server_out.as_str(),
        "--side",
        "server",
    ]);
    assert!(server_out.join("activation.rs").exists());
    assert!(server_out.join("mod.rs").exists());
    assert!(server_out.join("server.rs").exists());
    assert!(server_out.join("services.rs").exists());
    assert!(server_out.join("types.rs").exists());
    assert!(!server_out.join("client.rs").exists());
}

#[test]
fn generate_rust_accepts_notification_backend_options() {
    let temp = TempDir::new().expect("tempdir");
    let out = temp_path(&temp, "mpsc-sdk");

    run_tripley_rpc(&[
        "generate",
        "rust",
        "--in",
        kiosk_idl().as_str(),
        "--import-root",
        kiosk_root().as_str(),
        "--out",
        out.as_str(),
        "--notification-backend",
        "tokio-mpsc",
        "--notification-buffer",
        "16",
    ]);

    let client = std::fs::read_to_string(out.join("client.rs")).expect("read client");
    assert!(client.contains("use tokio::sync::mpsc;"));
    assert!(client.contains("pub struct CardReaderMediaInsertedReceiver"));
    assert!(!client.contains("use tokio::sync::broadcast;"));
}

#[test]
fn generate_typescript_writes_client_sdk_files() {
    let temp = TempDir::new().expect("tempdir");
    let out = temp_path(&temp, "typescript-sdk");

    run_tripley_rpc(&[
        "generate",
        "typescript",
        "--in",
        kiosk_idl().as_str(),
        "--import-root",
        kiosk_root().as_str(),
        "--out",
        out.as_str(),
        "--runtime-package",
        "@acme/xrpc-runtime",
    ]);

    for file in ["client.ts", "index.ts", "services.ts", "types.ts"] {
        assert!(out.join(file).exists(), "missing generated file {file}");
    }
    let client = std::fs::read_to_string(out.join("client.ts")).expect("read client");
    assert!(client.contains("from '@acme/xrpc-runtime'"));
    assert!(client.contains("export class CardReaderClient"));
}

#[test]
fn fmt_can_rewrite_and_check_directory() {
    let temp = TempDir::new().expect("tempdir");
    let idl = temp_path(&temp, "sample.rpc.yaml");
    std::fs::write(&idl, UNFORMATTED_IDL).expect("write idl");

    run_tripley_rpc(&["fmt", "--in", idl.as_str()]);
    run_tripley_rpc(&["fmt", "--in", temp_path(&temp, "").as_str(), "--check"]);
}

#[test]
fn doctor_runs_self_checks() {
    let output = run_tripley_rpc(&["doctor"]);
    assert!(String::from_utf8_lossy(&output.stdout).contains("doctor ok"));
}

fn run_tripley_rpc(args: &[&str]) -> std::process::Output {
    let output = Command::new(env!("CARGO_BIN_EXE_tripley-rpc"))
        .args(args)
        .current_dir(repo_root().as_str())
        .output()
        .expect("run tripley-rpc");
    if !output.status.success() {
        panic!(
            "tripley-rpc {} failed\nstdout:\n{}\nstderr:\n{}",
            args.join(" "),
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );
    }
    output
}

fn temp_path(temp: &TempDir, relative: &str) -> Utf8PathBuf {
    Utf8PathBuf::from_path_buf(temp.path().join(relative)).expect("utf8 temp path")
}

fn kiosk_idl() -> Utf8PathBuf {
    kiosk_root().join("card_reader.rpc.yaml")
}

fn kiosk_root() -> Utf8PathBuf {
    repo_root().join("idl/examples/kiosk_demo")
}

fn repo_root() -> Utf8PathBuf {
    Utf8Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .and_then(Utf8Path::parent)
        .expect("repo root")
        .to_owned()
}

const UNFORMATTED_IDL: &str = r#"
idl_version: 1
package: demo.format
types:
  Empty:
    kind: struct
    fields: []
services:
  Probe:
    guid: 42d85f4a-60b8-4b64-9ecb-1b6db17bd30c
    methods:
      ping:
        id: 1
        request: Empty
        response: Empty
"#;