use anyhow::{Context, Result};
use proc_macro2::TokenStream;
use quote::quote;
use super::emit_method::EmittedGuest;
use super::Behavior;
pub struct WrapperCrateInputs<'a> {
pub bindings_src: &'a str,
pub witty_impls: &'a [TokenStream],
pub guests: &'a [EmittedGuest],
pub behavior: Behavior,
pub strategy_crate_name: &'a str,
pub strategy_type: &'a str,
}
pub fn assemble_lib_rs(inputs: &WrapperCrateInputs<'_>) -> Result<String> {
let bindings_file =
syn::parse_file(inputs.bindings_src).context("could not parse bindings source as Rust")?;
let bindings_items = &bindings_file.items;
let strategy_crate_ident = syn::Ident::new(
&inputs.strategy_crate_name.replace('-', "_"),
proc_macro2::Span::call_site(),
);
let strategy_type_ident = syn::Ident::new(inputs.strategy_type, proc_macro2::Span::call_site());
let strategy_trait_use = match inputs.behavior {
Behavior::Transform => quote!(
use ::splicer_tool_sdk::TransformStrategy;
),
Behavior::Virtualize => quote!(
use ::splicer_tool_sdk::VirtualizeStrategy;
),
};
let witty_impls = inputs.witty_impls;
let args_structs: Vec<&TokenStream> = inputs
.guests
.iter()
.flat_map(|g| g.args_structs.iter())
.collect();
let guest_impls: Vec<&TokenStream> = inputs.guests.iter().map(|g| &g.guest_impl).collect();
let assembled = quote! {
mod bindings {
#(#bindings_items)*
}
use ::splicer_tool_sdk::{CallId, WitTyped};
use ::splicer_tool_sdk::wasm_wave::wasm::WasmValue;
#strategy_trait_use
#(#args_structs)*
#(#witty_impls)*
static STRATEGY: ::std::sync::OnceLock<#strategy_crate_ident::#strategy_type_ident> =
::std::sync::OnceLock::new();
fn strategy() -> &'static #strategy_crate_ident::#strategy_type_ident {
STRATEGY.get_or_init(
<#strategy_crate_ident::#strategy_type_ident as ::core::default::Default>::default
)
}
struct Wrapper;
#(#guest_impls)*
bindings::export!(Wrapper with_types_in bindings);
};
let parsed = syn::parse2::<syn::File>(assembled)
.context("assembled wrapper lib.rs is not parseable Rust")?;
Ok(prettyplease::unparse(&parsed))
}
pub struct CargoTomlInputs<'a> {
pub crate_name: &'a str,
pub strategy_crate_name: &'a str,
pub strategy_crate_path: &'a str,
pub splicer_tool_sdk_version: &'a str,
}
pub fn assemble_cargo_toml(inputs: &CargoTomlInputs<'_>) -> String {
use toml::map::Map;
use toml::Value;
let mut package = Map::new();
package.insert("name".into(), Value::String(inputs.crate_name.into()));
package.insert("version".into(), Value::String("0.1.0".into()));
package.insert("edition".into(), Value::String("2021".into()));
package.insert("publish".into(), Value::Boolean(false));
let mut lib = Map::new();
lib.insert(
"crate-type".into(),
Value::Array(vec![Value::String("cdylib".into())]),
);
let mut strategy_dep = Map::new();
strategy_dep.insert(
"path".into(),
Value::String(inputs.strategy_crate_path.into()),
);
let mut dependencies = Map::new();
dependencies.insert(
"splicer-tool-sdk".into(),
Value::String(inputs.splicer_tool_sdk_version.into()),
);
dependencies.insert(
inputs.strategy_crate_name.into(),
Value::Table(strategy_dep),
);
dependencies.insert("wit-bindgen".into(), Value::String("0.57".into()));
let mut root = Map::new();
root.insert("package".into(), Value::Table(package));
root.insert("lib".into(), Value::Table(lib));
root.insert("dependencies".into(), Value::Table(dependencies));
toml::to_string(&Value::Table(root))
.expect("toml serialization of strings + booleans + tables is infallible")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::adapter::typed::bindgen::run_wit_bindgen_rust;
use crate::adapter::typed::bindings_index::build_bindings_index;
use crate::adapter::typed::emit_method::emit_guest;
use crate::adapter::typed::emit_wit_typed::emit_wit_typed_impls;
use crate::adapter::typed::ir::build_ir;
const INTERFACE_QN: &str = "test:pkg/ops@0.1.0";
const TINY_WIT: &str = r#"
package test:pkg@0.1.0;
interface ops {
add: func(a: u32, b: u32) -> u32;
}
world w { export ops; }
"#;
fn assemble_for_wit(wit: &str, behavior: Behavior) -> String {
let (resolve, world_id, bindings_src) = run_wit_bindgen_rust(wit, Some("w")).unwrap();
let bindings = build_bindings_index(&bindings_src).unwrap();
let ir = build_ir(&resolve, world_id, &bindings).unwrap();
let user_impls = emit_wit_typed_impls(&ir.types);
let args_impls = emit_wit_typed_impls(&ir.args_records);
let witty_impls: Vec<_> = user_impls.into_iter().chain(args_impls).collect();
let guests: Vec<EmittedGuest> = bindings
.guest_traits
.iter()
.map(|g| emit_guest(g, INTERFACE_QN, behavior, &ir))
.collect();
let inputs = WrapperCrateInputs {
bindings_src: &bindings_src,
witty_impls: &witty_impls,
guests: &guests,
behavior,
strategy_crate_name: "my-strategy",
strategy_type: "MyStrategy",
};
assemble_lib_rs(&inputs).expect("assembly succeeds")
}
#[test]
fn assembled_lib_rs_has_required_pieces() {
let out = assemble_for_wit(TINY_WIT, Behavior::Transform);
assert!(
out.contains("mod bindings"),
"expected `mod bindings`:\n{out}"
);
assert!(
out.contains("use ::splicer_tool_sdk::TransformStrategy"),
"forward dispatch expects TransformStrategy use:\n{out}"
);
assert!(
out.contains("OnceLock"),
"expected OnceLock storage:\n{out}"
);
assert!(
out.contains("my_strategy :: MyStrategy") || out.contains("my_strategy::MyStrategy"),
"expected snake-cased strategy path:\n{out}"
);
assert!(out.contains("struct Wrapper"), "missing Wrapper:\n{out}");
assert!(
out.contains("bindings::export!"),
"expected bindings::export! line:\n{out}"
);
}
#[test]
fn virtualize_behavior_imports_virtualize_strategy() {
let out = assemble_for_wit(TINY_WIT, Behavior::Virtualize);
assert!(
out.contains("use ::splicer_tool_sdk::VirtualizeStrategy"),
"virtualize dispatch expects VirtualizeStrategy use:\n{out}"
);
assert!(
!out.contains("use ::splicer_tool_sdk::TransformStrategy"),
"virtualize emission should not import TransformStrategy:\n{out}"
);
}
#[test]
fn cargo_toml_lists_required_deps() {
let toml = assemble_cargo_toml(&CargoTomlInputs {
crate_name: "splicer_wrapper_test_pkg_ops_my_strategy",
strategy_crate_name: "my-strategy",
strategy_crate_path: "/abs/path/to/my-strategy",
splicer_tool_sdk_version: crate::test_consts::SDK_TEST_VERSION,
});
let parsed: toml::Value = toml::from_str(&toml).expect("Cargo.toml parses");
assert_eq!(
parsed["package"]["name"].as_str(),
Some("splicer_wrapper_test_pkg_ops_my_strategy")
);
assert_eq!(
parsed["lib"]["crate-type"]
.as_array()
.unwrap()
.first()
.and_then(|v| v.as_str()),
Some("cdylib")
);
assert_eq!(
parsed["dependencies"]["splicer-tool-sdk"].as_str(),
Some(crate::test_consts::SDK_TEST_VERSION),
"splicer-tool-sdk must be a plain registry version dep so cargo dedupes it with \
the strategy crate's own splicer-tool-sdk dep",
);
assert!(parsed["dependencies"].get("my-strategy").is_some());
assert!(parsed["dependencies"].get("wit-bindgen").is_some());
}
#[test]
fn cargo_toml_escapes_paths_with_special_chars() {
let toml = assemble_cargo_toml(&CargoTomlInputs {
crate_name: "wrapper",
strategy_crate_name: "strat",
strategy_crate_path: r#"C:\Users\me\strat-"with-quote""#,
splicer_tool_sdk_version: crate::test_consts::SDK_TEST_VERSION,
});
let parsed: toml::Value =
toml::from_str(&toml).expect("Cargo.toml with special-char paths still parses");
assert_eq!(
parsed["dependencies"]["strat"]["path"].as_str(),
Some(r#"C:\Users\me\strat-"with-quote""#),
);
}
}