use std::{collections::HashMap, env, fs, path::PathBuf};
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{ToTokens, quote};
use syn::{
Attribute, Error, File, FnArg, ImplItem, ImplItemFn, Item, ItemFn, ItemImpl, ItemStruct,
LitStr, Pat, ReturnType, Token, TypePath,
parse::{Parse, ParseStream},
parse_quote,
spanned::Spanned,
visit_mut::VisitMut,
};
use wit_bindgen_core::wit_parser::{PackageId, Resolve, UnresolvedPackageGroup};
use wit_bindgen_rust::{Opts, WithOption};
use crate::manifest_paths;
const WRAPPER_STRUCT_NAME: &str = "Account";
#[derive(Default)]
struct GenerateArgs {
inline: Option<LitStr>,
with_entries: Vec<(String, WithOption)>,
}
fn parse_with_entry(input: ParseStream<'_>) -> syn::Result<(String, WithOption)> {
let key: LitStr = input.parse()?;
input.parse::<Token![:]>()?;
let path: syn::Path = input.parse()?;
let option = if path.leading_colon.is_none()
&& path.segments.len() == 1
&& path.segments.first().is_some_and(|seg| seg.ident == "generate")
{
WithOption::Generate
} else {
let path_str = path.to_token_stream().to_string().replace(' ', "");
WithOption::Path(path_str)
};
Ok((key.value(), option))
}
impl Parse for GenerateArgs {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut args = GenerateArgs::default();
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
let name = ident.to_string();
input.parse::<Token![=]>()?;
if name == "inline" {
if args.inline.is_some() {
return Err(syn::Error::new(ident.span(), "duplicate `inline` argument"));
}
args.inline = Some(input.parse()?);
} else if name == "with" {
if !args.with_entries.is_empty() {
return Err(syn::Error::new(ident.span(), "duplicate `with` argument"));
}
let content;
syn::braced!(content in input);
while !content.is_empty() {
args.with_entries.push(parse_with_entry(&content)?);
if content.peek(Token![,]) {
content.parse::<Token![,]>()?;
}
}
} else {
return Err(syn::Error::new(
ident.span(),
format!("unsupported generate! argument `{name}`"),
));
}
if input.peek(Token![,]) {
let _ = input.parse::<Token![,]>()?;
}
}
Ok(args)
}
}
pub(crate) fn expand(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input_tokens: proc_macro2::TokenStream = input.into();
let args = if input_tokens.is_empty() {
GenerateArgs::default()
} else {
match syn::parse2::<GenerateArgs>(input_tokens) {
Ok(parsed) => parsed,
Err(err) => return err.to_compile_error().into(),
}
};
let resolve_opts = manifest_paths::ResolveOptions {
allow_missing_local_wit: args.inline.is_some(),
};
match manifest_paths::resolve_wit_paths(resolve_opts) {
Ok(config) => {
if config.paths.is_empty() {
return Error::new(
Span::call_site(),
"no WIT dependencies declared under \
[package.metadata.component.target.dependencies]",
)
.to_compile_error()
.into();
}
let inline_world = args
.inline
.as_ref()
.and_then(|src| manifest_paths::extract_world_name(&src.value()));
let world_value = inline_world.or_else(|| config.world.clone());
if args.inline.is_some() && world_value.is_none() {
return Error::new(
Span::call_site(),
"failed to detect world name for inline WIT provided to generate!",
)
.to_compile_error()
.into();
}
match generate_bindings(&args, &config, world_value.as_deref()) {
Ok(raw_bindings) => match augment_generated_bindings(raw_bindings) {
Ok(augmented) => {
quote! {
#[doc(hidden)]
#[allow(dead_code)]
pub mod bindings {
#augmented
}
}
.into()
}
Err(err) => err.to_compile_error().into(),
},
Err(err) => err.to_compile_error().into(),
}
}
Err(err) => err.to_compile_error().into(),
}
}
fn generate_bindings(
args: &GenerateArgs,
config: &manifest_paths::ResolvedWit,
world: Option<&str>,
) -> Result<TokenStream2, Error> {
let inline_src = args.inline.as_ref().map(|src| src.value());
let inline_ref = inline_src.as_deref();
let wit_sources = load_wit_sources(&config.paths, inline_ref)?;
let world_id = wit_sources
.resolve
.select_world(&wit_sources.packages, world)
.map_err(|err| Error::new(Span::call_site(), err.to_string()))?;
let mut opts = Opts {
generate_all: true,
runtime_path: Some("::miden::wit_bindgen::rt".to_string()),
default_bindings_module: Some("bindings".to_string()),
..Opts::default()
};
push_custom_with_entries(&mut opts, &args.with_entries);
push_default_with_entries(&mut opts);
let mut generated_files = wit_bindgen_core::Files::default();
let mut generator = opts.build();
generator
.generate(&wit_sources.resolve, world_id, &mut generated_files)
.map_err(|err| Error::new(Span::call_site(), err.to_string()))?;
let (_, src_bytes) = generated_files
.iter()
.next()
.ok_or_else(|| Error::new(Span::call_site(), "wit-bindgen emitted no bindings"))?;
let src = std::str::from_utf8(src_bytes)
.map_err(|err| Error::new(Span::call_site(), format!("invalid UTF-8: {err}")))?;
let mut tokens: TokenStream2 = src
.parse()
.map_err(|err| Error::new(Span::call_site(), format!("failed to parse bindings: {err}")))?;
for path in wit_sources.files_read {
let utf8_path = path.to_str().ok_or_else(|| {
Error::new(
Span::call_site(),
format!("path '{}' contains invalid UTF-8", path.display()),
)
})?;
tokens.extend(quote! {
const _: &[u8] = include_bytes!(#utf8_path);
});
}
Ok(tokens)
}
fn augment_generated_bindings(tokens: TokenStream2) -> syn::Result<TokenStream2> {
let mut file: File = syn::parse2(tokens)?;
let mut collected_methods = Vec::new();
collect_wrapper_methods(&file.items, &mut Vec::new(), &mut collected_methods)?;
check_method_name_collisions(&collected_methods)?;
if !collected_methods.is_empty() {
let struct_ident = syn::Ident::new(WRAPPER_STRUCT_NAME, Span::call_site());
let struct_item: ItemStruct = parse_quote! {
#[derive(Default)]
pub struct #struct_ident;
};
let mut impl_item: ItemImpl = parse_quote! {
impl #struct_ident {}
};
impl_item
.items
.extend(collected_methods.into_iter().map(|cm| ImplItem::Fn(cm.method)));
file.items.push(Item::Struct(struct_item));
file.items.push(Item::Impl(impl_item));
}
Ok(file.into_token_stream())
}
struct LoadedWitSources {
resolve: Resolve,
packages: Vec<PackageId>,
files_read: Vec<PathBuf>,
}
fn load_wit_sources(
paths: &[String],
inline_source: Option<&str>,
) -> Result<LoadedWitSources, Error> {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|err| {
Error::new(Span::call_site(), format!("failed to read CARGO_MANIFEST_DIR: {err}"))
})?;
let manifest_dir = PathBuf::from(manifest_dir);
let mut resolve = Resolve::default();
let mut packages = Vec::new();
let mut files = Vec::new();
for path in paths {
let path_buf = PathBuf::from(path);
let absolute = if path_buf.is_absolute() {
path_buf
} else {
manifest_dir.join(path_buf)
};
let normalized = fs::canonicalize(&absolute).unwrap_or(absolute);
let (pkg, sources) = resolve.push_path(normalized.clone()).map_err(|err| {
Error::new(
Span::call_site(),
format!("failed to load WIT from '{}': {err}", normalized.display()),
)
})?;
packages.push(pkg);
files.extend(sources.paths().map(|p| p.to_owned()));
}
if let Some(src) = inline_source {
packages.clear();
let group = UnresolvedPackageGroup::parse("inline", src)
.map_err(|err| Error::new(Span::call_site(), err.to_string()))?;
let pkg = resolve
.push_group(group)
.map_err(|err| Error::new(Span::call_site(), err.to_string()))?;
packages.push(pkg);
}
Ok(LoadedWitSources {
resolve,
packages,
files_read: files,
})
}
fn push_custom_with_entries(opts: &mut Opts, entries: &[(String, WithOption)]) {
opts.with.extend(entries.iter().cloned());
}
fn push_default_with_entries(opts: &mut Opts) {
opts.with
.push(("miden:base/core-types@1.0.0".to_string(), WithOption::Generate));
push_path_entry(opts, "miden:base/core-types@1.0.0/felt", "::miden::Felt");
push_path_entry(opts, "miden:base/core-types@1.0.0/word", "::miden::Word");
push_path_entry(opts, "miden:base/core-types@1.0.0/asset", "::miden::Asset");
push_path_entry(opts, "miden:base/core-types@1.0.0/account-id", "::miden::AccountId");
push_path_entry(opts, "miden:base/core-types@1.0.0/tag", "::miden::Tag");
push_path_entry(opts, "miden:base/core-types@1.0.0/note-type", "::miden::NoteType");
push_path_entry(opts, "miden:base/core-types@1.0.0/recipient", "::miden::Recipient");
push_path_entry(opts, "miden:base/core-types@1.0.0/note-idx", "::miden::NoteIdx");
}
fn push_path_entry(opts: &mut Opts, key: &str, value: &str) {
opts.with.push((key.to_string(), WithOption::Path(value.to_string())));
}
struct CollectedMethod {
method: ImplItemFn,
source_path: String,
}
fn collect_wrapper_methods(
items: &[Item],
path: &mut Vec<syn::Ident>,
methods_out: &mut Vec<CollectedMethod>,
) -> syn::Result<()> {
for item in items.iter() {
if let Item::Mod(module) = item {
path.push(module.ident.clone());
if let Some((_, ref content)) = module.content {
collect_wrapper_methods(content, path, methods_out)?;
collect_methods_from_module(content, path, methods_out)?;
}
path.pop();
}
}
Ok(())
}
fn collect_methods_from_module(
items: &[Item],
path: &[syn::Ident],
methods_out: &mut Vec<CollectedMethod>,
) -> syn::Result<()> {
if !should_generate_struct(path, items) {
return Ok(());
}
let functions: Vec<&ItemFn> = items
.iter()
.filter_map(|item| match item {
Item::Fn(func) if is_target_function(func) => Some(func),
_ => None,
})
.collect();
let source_path = format_module_path(path);
for func in functions {
methods_out.push(CollectedMethod {
method: build_wrapper_method(func, path)?,
source_path: source_path.clone(),
});
}
Ok(())
}
fn build_wrapper_method(func: &ItemFn, module_path: &[syn::Ident]) -> syn::Result<ImplItemFn> {
let mut sig = func.sig.clone();
sig.inputs.insert(0, parse_quote!(&mut self));
qualify_signature_types(&mut sig, module_path);
let arg_idents = collect_arg_idents(func)?;
let call_expr = wrapper_call_tokens(module_path, &sig.ident, &arg_idents);
let method_doc = format!("Calls `{}` from `{}`.", sig.ident, format_module_path(module_path));
let doc_attr: Attribute = parse_quote!(#[doc = #method_doc]);
let inline_attr: Attribute = parse_quote!(#[inline(always)]);
let body_tokens = match &sig.output {
ReturnType::Default => quote!({ #call_expr; }),
_ => quote!({ #call_expr }),
};
let block = syn::parse2(body_tokens)?;
Ok(ImplItemFn {
attrs: vec![doc_attr, inline_attr],
vis: func.vis.clone(),
defaultness: None,
sig,
block,
})
}
fn qualify_signature_types(sig: &mut syn::Signature, module_path: &[syn::Ident]) {
struct TypeQualifier<'a> {
module_path: &'a [syn::Ident],
}
impl VisitMut for TypeQualifier<'_> {
fn visit_type_path_mut(&mut self, type_path: &mut TypePath) {
if type_path.qself.is_none()
&& type_path.path.leading_colon.is_none()
&& type_path.path.segments.len() == 1
{
let first_segment = &type_path.path.segments[0].ident;
let name = first_segment.to_string();
if is_primitive_or_std_type(&name) {
return;
}
let mut new_segments = syn::punctuated::Punctuated::new();
for ident in self.module_path {
new_segments.push(syn::PathSegment {
ident: ident.clone(),
arguments: syn::PathArguments::None,
});
}
new_segments.push(type_path.path.segments[0].clone());
type_path.path.segments = new_segments;
}
syn::visit_mut::visit_type_path_mut(self, type_path);
}
}
let mut qualifier = TypeQualifier { module_path };
qualifier.visit_signature_mut(sig);
}
fn is_primitive_or_std_type(name: &str) -> bool {
matches!(
name,
"bool"
| "char"
| "str"
| "u8"
| "u16"
| "u32"
| "u64"
| "u128"
| "usize"
| "i8"
| "i16"
| "i32"
| "i64"
| "i128"
| "isize"
| "f32"
| "f64"
| "String"
| "Vec"
| "Option"
| "Result"
| "Self"
)
}
fn collect_arg_idents(func: &ItemFn) -> syn::Result<Vec<syn::Ident>> {
func.sig
.inputs
.iter()
.map(|arg| match arg {
FnArg::Receiver(_) => {
Err(Error::new(func.sig.ident.span(), "unexpected receiver in generated function"))
}
FnArg::Typed(pat_type) => match pat_type.pat.as_ref() {
Pat::Ident(pat_ident) => Ok(pat_ident.ident.clone()),
other => Err(Error::new(
other.span(),
format!(
"unsupported argument pattern `{}` in generated function",
quote!(#other)
),
)),
},
})
.collect()
}
fn wrapper_call_tokens(
module_path: &[syn::Ident],
fn_ident: &syn::Ident,
args: &[syn::Ident],
) -> TokenStream2 {
let mut path_tokens = quote! { crate::bindings };
for ident in module_path {
path_tokens = quote! { #path_tokens :: #ident };
}
quote! { #path_tokens :: #fn_ident(#(#args),*) }
}
fn should_generate_struct(path: &[syn::Ident], items: &[Item]) -> bool {
if path.is_empty() {
return false;
}
let first = path[0].to_string();
if first == "exports" {
return false;
}
if first.starts_with('_') {
return false;
}
let last = path.last().unwrap().to_string();
if last.starts_with('_') {
return false;
}
!items.iter().any(|item| matches!(item, Item::Mod(_)))
}
fn is_target_function(func: &ItemFn) -> bool {
matches!(func.vis, syn::Visibility::Public(_))
&& func.sig.unsafety.is_none()
&& !func.sig.ident.to_string().starts_with('_')
}
fn format_module_path(path: &[syn::Ident]) -> String {
path.iter().map(|ident| ident.to_string()).collect::<Vec<_>>().join("::")
}
fn check_method_name_collisions(methods: &[CollectedMethod]) -> syn::Result<()> {
let mut seen: HashMap<String, &str> = HashMap::new();
for collected in methods {
let method_name = collected.method.sig.ident.to_string();
if let Some(existing_path) = seen.get(&method_name) {
return Err(Error::new(
Span::call_site(),
format!(
"method name collision in generated `{WRAPPER_STRUCT_NAME}` struct: \
`{method_name}` is defined in both `{existing_path}` and `{}`. Consider \
using the original module paths directly instead of the wrapper struct.",
collected.source_path
),
));
}
seen.insert(method_name, &collected.source_path);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_file(src: &str) -> File {
syn::parse_str(src).unwrap_or_else(|e| panic!("failed to parse test source: {e}\n{src}"))
}
#[test]
fn test_should_generate_struct_empty_path() {
let empty_items: Vec<Item> = vec![];
assert!(!should_generate_struct(&[], &empty_items));
}
#[test]
fn test_should_generate_struct_exports_excluded() {
let empty_items: Vec<Item> = vec![];
let path = vec![syn::Ident::new("exports", Span::call_site())];
assert!(!should_generate_struct(&path, &empty_items));
let path = vec![
syn::Ident::new("exports", Span::call_site()),
syn::Ident::new("foo", Span::call_site()),
];
assert!(!should_generate_struct(&path, &empty_items));
}
#[test]
fn test_should_generate_struct_underscore_excluded() {
let empty_items: Vec<Item> = vec![];
let path = vec![syn::Ident::new("_private", Span::call_site())];
assert!(!should_generate_struct(&path, &empty_items));
let path = vec![
syn::Ident::new("miden", Span::call_site()),
syn::Ident::new("_internal", Span::call_site()),
];
assert!(!should_generate_struct(&path, &empty_items));
}
#[test]
fn test_should_generate_struct_valid_leaf_modules() {
let empty_items: Vec<Item> = vec![];
let path = vec![syn::Ident::new("miden", Span::call_site())];
assert!(should_generate_struct(&path, &empty_items));
let path = vec![
syn::Ident::new("miden", Span::call_site()),
syn::Ident::new("basic_wallet", Span::call_site()),
];
assert!(should_generate_struct(&path, &empty_items));
}
#[test]
fn test_should_generate_struct_non_leaf_excluded() {
let path = vec![syn::Ident::new("miden", Span::call_site())];
let items_with_mod: Vec<Item> = vec![syn::parse_quote! { mod nested {} }];
assert!(!should_generate_struct(&path, &items_with_mod));
let items_with_fn: Vec<Item> = vec![syn::parse_quote! { pub fn foo() {} }];
assert!(should_generate_struct(&path, &items_with_fn));
}
#[test]
fn test_is_target_function_public() {
let func: ItemFn = syn::parse_quote! {
pub fn receive_asset(asset: u64) {}
};
assert!(is_target_function(&func));
}
#[test]
fn test_is_target_function_private_excluded() {
let func: ItemFn = syn::parse_quote! {
fn private_fn() {}
};
assert!(!is_target_function(&func));
}
#[test]
fn test_is_target_function_unsafe_excluded() {
let func: ItemFn = syn::parse_quote! {
pub unsafe fn unsafe_fn() {}
};
assert!(!is_target_function(&func));
}
#[test]
fn test_is_target_function_underscore_excluded() {
let func: ItemFn = syn::parse_quote! {
pub fn _internal() {}
};
assert!(!is_target_function(&func));
}
#[test]
fn test_format_module_path() {
let path = vec![
syn::Ident::new("miden", Span::call_site()),
syn::Ident::new("basic_wallet", Span::call_site()),
];
assert_eq!(format_module_path(&path), "miden::basic_wallet");
}
#[test]
fn test_format_module_path_empty() {
assert_eq!(format_module_path(&[]), "");
}
#[test]
fn test_collect_arg_idents() {
let func: ItemFn = syn::parse_quote! {
pub fn foo(a: u32, b: String, c: Vec<u8>) {}
};
let idents = collect_arg_idents(&func).unwrap();
let names: Vec<_> = idents.iter().map(|i| i.to_string()).collect();
assert_eq!(names, vec!["a", "b", "c"]);
}
#[test]
fn test_collect_arg_idents_empty() {
let func: ItemFn = syn::parse_quote! {
pub fn no_args() {}
};
let idents = collect_arg_idents(&func).unwrap();
assert!(idents.is_empty());
}
#[test]
fn test_collect_wrapper_methods_from_leaf_module() {
let src = r#"
mod miden {
mod basic_wallet {
mod basic_wallet {
pub fn receive_asset(asset: u64) {}
pub fn send_asset(asset: u64) {}
}
}
}
"#;
let file = parse_file(src);
let mut methods = Vec::new();
collect_wrapper_methods(&file.items, &mut Vec::new(), &mut methods).unwrap();
assert_eq!(methods.len(), 2);
let method_names: Vec<_> = methods.iter().map(|m| m.method.sig.ident.to_string()).collect();
assert!(method_names.contains(&"receive_asset".to_string()));
assert!(method_names.contains(&"send_asset".to_string()));
}
#[test]
fn test_collect_wrapper_methods_skips_exports() {
let src = r#"
mod exports {
mod my_component {
pub fn exported_fn() {}
}
}
"#;
let file = parse_file(src);
let mut methods = Vec::new();
collect_wrapper_methods(&file.items, &mut Vec::new(), &mut methods).unwrap();
assert!(methods.is_empty());
}
#[test]
fn test_collect_wrapper_methods_skips_empty_modules() {
let src = r#"
mod miden {
mod empty_module {
}
}
"#;
let file = parse_file(src);
let mut methods = Vec::new();
collect_wrapper_methods(&file.items, &mut Vec::new(), &mut methods).unwrap();
assert!(methods.is_empty());
}
#[test]
fn test_qualify_signature_types() {
let func: ItemFn = syn::parse_quote! {
pub fn test_fn(a: StructA, b: u64) -> StructB {}
};
let path = vec![
syn::Ident::new("miden", Span::call_site()),
syn::Ident::new("component", Span::call_site()),
];
let method = build_wrapper_method(&func, &path).unwrap();
let sig_str = method.sig.to_token_stream().to_string();
assert!(sig_str.contains("miden :: component :: StructA"));
assert!(sig_str.contains("miden :: component :: StructB"));
assert!(sig_str.contains("u64"));
assert!(!sig_str.contains("miden :: component :: u64"));
}
#[test]
fn test_build_wrapper_method_signature() {
let func: ItemFn = syn::parse_quote! {
pub fn receive_asset(asset: u64) {}
};
let path = vec![
syn::Ident::new("miden", Span::call_site()),
syn::Ident::new("basic_wallet", Span::call_site()),
];
let method = build_wrapper_method(&func, &path).unwrap();
assert_eq!(method.sig.inputs.len(), 2);
assert!(
matches!(method.sig.inputs.first(), Some(FnArg::Receiver(r)) if r.mutability.is_some())
);
assert!(matches!(method.vis, syn::Visibility::Public(_)));
assert!(method.attrs.iter().any(|attr| { attr.path().is_ident("inline") }));
}
#[test]
fn test_build_wrapper_method_with_return_type() {
let func: ItemFn = syn::parse_quote! {
pub fn get_value() -> u32 { 42 }
};
let path = vec![syn::Ident::new("test_mod", Span::call_site())];
let method = build_wrapper_method(&func, &path).unwrap();
assert!(matches!(method.sig.output, ReturnType::Type(_, _)));
}
#[test]
fn test_augment_generated_bindings_adds_account_struct() {
let src = r#"
mod miden {
mod basic_wallet {
mod basic_wallet {
pub fn receive_asset(asset: u64) {}
pub fn send_asset(to: u32, amount: u64) -> bool { true }
}
}
}
"#;
let tokens: TokenStream2 = src.parse().unwrap();
let result = augment_generated_bindings(tokens).unwrap();
let result_str = result.to_string();
assert!(result_str.contains("struct Account"));
assert!(result_str.contains("impl Account"));
assert!(result_str.contains("fn receive_asset"));
assert!(result_str.contains("fn send_asset"));
assert!(result_str.contains("& mut self"));
}
#[test]
fn test_augment_generated_bindings_empty_input() {
let src = "";
let tokens: TokenStream2 = src.parse().unwrap();
let result = augment_generated_bindings(tokens).unwrap();
let result_str = result.to_string();
assert!(!result_str.contains("struct Account"));
}
#[test]
fn test_augment_generated_bindings_exports_only() {
let src = r#"
mod exports {
mod my_component {
pub fn exported_fn() {}
}
}
"#;
let tokens: TokenStream2 = src.parse().unwrap();
let result = augment_generated_bindings(tokens).unwrap();
let result_str = result.to_string();
assert!(!result_str.contains("struct Account"));
}
#[test]
fn test_augment_generated_bindings_preserves_original_modules() {
let src = r#"
mod miden {
mod wallet {
pub fn get_balance() -> u64 { 0 }
}
}
"#;
let tokens: TokenStream2 = src.parse().unwrap();
let result = augment_generated_bindings(tokens).unwrap();
let result_str = result.to_string();
assert!(result_str.contains("mod miden"));
assert!(result_str.contains("mod wallet"));
assert!(result_str.contains("fn get_balance"));
}
#[test]
fn test_wrapper_call_tokens_generates_correct_path() {
let path = vec![
syn::Ident::new("miden", Span::call_site()),
syn::Ident::new("basic_wallet", Span::call_site()),
];
let fn_ident = syn::Ident::new("receive_asset", Span::call_site());
let args = vec![syn::Ident::new("asset", Span::call_site())];
let tokens = wrapper_call_tokens(&path, &fn_ident, &args);
let result = tokens.to_string();
assert!(result.contains("crate :: bindings :: miden :: basic_wallet :: receive_asset"));
assert!(result.contains("asset"));
}
#[test]
fn test_parse_with_entry_generate() {
let input: TokenStream2 = quote! { "miden:foo/bar": generate };
let parsed = syn::parse2::<GenerateArgs>(quote! { with = { #input } }).unwrap();
assert_eq!(parsed.with_entries.len(), 1);
assert_eq!(parsed.with_entries[0].0, "miden:foo/bar");
assert!(matches!(parsed.with_entries[0].1, WithOption::Generate));
}
#[test]
fn test_parse_with_entry_path() {
let input: TokenStream2 = quote! { "miden:foo/bar": ::my::custom::Type };
let parsed = syn::parse2::<GenerateArgs>(quote! { with = { #input } }).unwrap();
assert_eq!(parsed.with_entries.len(), 1);
assert_eq!(parsed.with_entries[0].0, "miden:foo/bar");
match &parsed.with_entries[0].1 {
WithOption::Path(p) => assert_eq!(p, "::my::custom::Type"),
_ => panic!("expected Path variant"),
}
}
#[test]
fn test_parse_multiple_with_entries() {
let parsed = syn::parse2::<GenerateArgs>(quote! {
with = {
"miden:a/b": generate,
"miden:c/d": ::foo::Bar
}
})
.unwrap();
assert_eq!(parsed.with_entries.len(), 2);
assert_eq!(parsed.with_entries[0].0, "miden:a/b");
assert_eq!(parsed.with_entries[1].0, "miden:c/d");
}
#[test]
fn test_augment_generated_bindings_integration() {
let src = r#"
mod miden {
mod basic_wallet {
mod basic_wallet {
pub struct AssetInfo {
pub amount: u64,
}
pub fn receive_asset(asset: AssetInfo) {}
pub fn move_asset_to_note(asset: AssetInfo, note_idx: u32) -> bool { true }
fn _internal_helper() {} // Should be skipped (underscore prefix)
}
}
mod other_component {
mod other_component {
pub fn do_something(value: u64) -> u64 { value }
}
}
}
mod exports {
mod my_export {
pub fn exported_fn() {} // Should be skipped (exports module)
}
}
"#;
let tokens: TokenStream2 = src.parse().unwrap();
let result = augment_generated_bindings(tokens).unwrap();
let parsed: File =
syn::parse2(result.clone()).expect("augmented bindings should be valid Rust syntax");
let has_account_struct = parsed
.items
.iter()
.any(|item| matches!(item, Item::Struct(s) if s.ident == "Account"));
let has_account_impl = parsed.items.iter().any(|item| {
matches!(item, Item::Impl(i) if i.self_ty.to_token_stream().to_string() == "Account")
});
assert!(has_account_struct, "should generate Account struct");
assert!(has_account_impl, "should generate Account impl");
let impl_block = parsed
.items
.iter()
.find_map(|item| match item {
Item::Impl(i) if i.self_ty.to_token_stream().to_string() == "Account" => Some(i),
_ => None,
})
.expect("Account impl should exist");
let method_names: Vec<String> = impl_block
.items
.iter()
.filter_map(|item| match item {
ImplItem::Fn(f) => Some(f.sig.ident.to_string()),
_ => None,
})
.collect();
assert!(method_names.contains(&"receive_asset".to_string()));
assert!(method_names.contains(&"move_asset_to_note".to_string()));
assert!(method_names.contains(&"do_something".to_string()));
assert!(!method_names.contains(&"_internal_helper".to_string()));
assert!(!method_names.contains(&"exported_fn".to_string()));
let result_str = result.to_string();
assert!(
result_str.contains("miden :: basic_wallet :: basic_wallet :: AssetInfo"),
"custom types should be qualified with module path"
);
}
#[test]
fn test_method_name_collision_detected() {
let src = r#"
mod miden {
mod interface_a {
mod interface_a {
pub fn transfer(amount: u64) {}
}
}
mod interface_b {
mod interface_b {
pub fn transfer(value: u32) {}
}
}
}
"#;
let tokens: TokenStream2 = src.parse().unwrap();
let result = augment_generated_bindings(tokens);
assert!(result.is_err(), "should detect method name collision");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("method name collision"),
"error should mention collision: {err_msg}"
);
assert!(err_msg.contains("transfer"), "error should mention the colliding method name");
}
#[test]
fn test_no_collision_different_names() {
let src = r#"
mod miden {
mod interface_a {
mod interface_a {
pub fn transfer_a(amount: u64) {}
}
}
mod interface_b {
mod interface_b {
pub fn transfer_b(value: u32) {}
}
}
}
"#;
let tokens: TokenStream2 = src.parse().unwrap();
let result = augment_generated_bindings(tokens);
assert!(result.is_ok(), "should not detect collision for different method names");
}
}