use std::io::Write;
use std::process::{Command, Stdio};
use camino::{Utf8Path, Utf8PathBuf};
use rpc_gen_cpp::{GenerateOptions, GenerateSide, generate, generate_to_dir};
use rpc_idl_ir::{CanonicalIr, build_ir, to_pretty_json};
use rpc_idl_loader::{LoadOptions, load_graph};
use rpc_idl_semantic::analyze;
use tempfile::TempDir;
#[test]
fn generate_kiosk_cpp_sdk_files() {
let ir = compile_kiosk();
let files = generate(&ir, &GenerateOptions::default()).expect("generate");
assert!(files.contains_key("CMakeLists.txt"));
assert!(files.contains_key("include/tripley_rpc_generated/types.h"));
assert!(files.contains_key("include/tripley_rpc_generated/services.h"));
assert!(files.contains_key("include/tripley_rpc_generated/client.h"));
assert!(files.contains_key("include/tripley_rpc_generated/server.h"));
assert!(files.contains_key("include/tripley_rpc_generated/activation.h"));
let types = &files["include/tripley_rpc_generated/types.h"];
assert!(types.contains("struct CardInfo"));
assert!(types.contains("std::optional<std::vector<std::uint8_t>> tracks;"));
assert!(types.contains("enum class DeviceStatus : std::int32_t"));
assert!(types.contains("MSGPACK_DEFINE_ARRAY"));
let client = &files["include/tripley_rpc_generated/client.h"];
assert!(client.contains("class CardReaderClient"));
assert!(client.contains("ResolveNamed"));
assert!(client.contains("Create"));
assert!(client.contains("SubscribeMediaInserted"));
assert!(!client.contains("std::optional<std::optional"));
assert!(files["src/client.cc"].contains("CardReaderClient::ResolveNamed"));
let server = &files["include/tripley_rpc_generated/server.h"];
assert!(server.contains("class CardReaderService"));
assert!(server.contains("RegisterCardReaderNamed"));
}
#[test]
fn generate_side_filtering_works() {
let ir = compile_kiosk();
let client = generate(
&ir,
&GenerateOptions {
side: GenerateSide::Client,
..GenerateOptions::default()
},
)
.expect("client generate");
assert!(client.contains_key("include/tripley_rpc_generated/client.h"));
assert!(!client.contains_key("include/tripley_rpc_generated/server.h"));
let server = generate(
&ir,
&GenerateOptions {
side: GenerateSide::Server,
..GenerateOptions::default()
},
)
.expect("server generate");
assert!(server.contains_key("include/tripley_rpc_generated/server.h"));
assert!(!server.contains_key("include/tripley_rpc_generated/client.h"));
}
#[test]
fn generate_typed_constructor_cpp_sdk_files() {
let temp = TempDir::new().expect("tempdir");
let idl = temp_path(&temp, "session.rpc.yaml");
std::fs::write(&idl, CONSTRUCTOR_IDL).expect("write idl");
let ir = compile(&idl, Vec::new());
let files = generate(&ir, &GenerateOptions::default()).expect("generate");
let client_h = &files["include/tripley_rpc_generated/client.h"];
assert!(client_h.contains(
"Create(tripley::rpc::RpcClient& client, const demo::session::SessionReaderCreateArgs& request"
));
let client_cc = &files["src/client.cc"];
assert!(client_cc.contains("tripley::rpc::EncodePayload(request, zone)"));
assert!(client_cc.contains("client.CreateInstance"));
let server_h = &files["include/tripley_rpc_generated/server.h"];
assert!(server_h.contains("class SessionReaderFactory"));
assert!(server_h.contains(
"Create(tripley::rpc::RpcCallContext& ctx, const demo::session::SessionReaderCreateArgs& request"
));
assert!(server_h.contains("missing constructor payload"));
assert!(server_h.contains("DecodePayload<demo::session::SessionReaderCreateArgs>"));
}
#[test]
fn generate_to_dir_writes_expected_files() {
let temp = TempDir::new().expect("tempdir");
let out = temp_path(&temp, "cpp-sdk");
generate_to_dir(&compile_kiosk(), &out, GenerateOptions::default()).expect("generate to dir");
assert!(out.join("CMakeLists.txt").exists());
assert!(out.join("include/tripley_rpc_generated/types.h").exists());
assert!(out.join("src/types.cc").exists());
}
#[test]
fn plugin_describe_reports_cpp_capabilities() {
let output = Command::new(env!("CARGO_BIN_EXE_rpc-gen-cpp"))
.arg("--describe")
.output()
.expect("run describe");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("\"plugin_name\":\"cpp\""));
assert!(stdout.contains("\"cpp.client\""));
assert!(stdout.contains("\"tcp\""));
}
#[test]
fn plugin_generates_from_stdin_ir() {
let temp = TempDir::new().expect("tempdir");
let out = temp_path(&temp, "plugin-sdk");
let ir = to_pretty_json(&compile_kiosk()).expect("ir json");
let mut child = Command::new(env!("CARGO_BIN_EXE_rpc-gen-cpp"))
.arg("--out-dir")
.arg(out.as_str())
.arg("--opt")
.arg("include_prefix=acme/generated")
.stdin(Stdio::piped())
.spawn()
.expect("spawn plugin");
child
.stdin
.as_mut()
.expect("stdin")
.write_all(ir.as_bytes())
.expect("write ir");
let output = child.wait_with_output().expect("wait plugin");
assert!(output.status.success());
assert!(out.join("include/acme/generated/client.h").exists());
}
fn compile_kiosk() -> CanonicalIr {
compile(&kiosk_idl(), vec![kiosk_root()])
}
fn compile(input: &Utf8Path, import_roots: Vec<Utf8PathBuf>) -> CanonicalIr {
let graph = load_graph(input, LoadOptions { import_roots }).expect("load idl");
let model = analyze(&graph).expect("analyze idl");
build_ir(&model)
}
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 CONSTRUCTOR_IDL: &str = r#"
idl_version: 1
package: demo.session
types:
Empty:
kind: struct
fields: []
SessionReaderCreateArgs:
kind: struct
fields:
- id: 0
name: token
type: string
SessionInfo:
kind: struct
fields:
- id: 0
name: token
type: string
services:
SessionReader:
guid: 4edfea20-9929-448d-94c9-4b28f5f8a2c5
options:
rpc.activation_modes: [instantiable]
create:
request: SessionReaderCreateArgs
methods:
whoami:
id: 1
request: Empty
response: SessionInfo
"#;