use heck::ToLowerCamelCase; use proc_macro::TokenStream; use quote::quote; use std::collections::BTreeSet; use syn::{ parse::Parse, parse::ParseStream, parse_macro_input, punctuated::Punctuated, token::Comma, Expr, FnArg, Ident, ItemFn, Lit, MetaNameValue, Pat, PathArguments, ReturnType, Type, }; use crate::attr::MacroArgs; mod attr; fn get_lit_str_value(nv: &MetaNameValue) -> Option { // In syn v2 MetaNameValue.value is an Expr (usually Expr::Lit). Extract string literal if present. match &nv.value { Expr::Lit(expr_lit) => { if let Lit::Str(s) = &expr_lit.lit { Some(s.value()) } else { None } } _ => None, } } fn is_tauri_native(ty: &Type) -> bool { // Unwrap reference let mut t = ty; if let Type::Reference(r) = t { t = &*r.elem; } if let Type::Path(p) = t { if let Some(seg) = p.path.segments.last() { let ident = seg.ident.to_string(); if ident == "Window" || ident == "State" { return true; } } } false } fn extract_ident_from_type(ty: &Type) -> Option { // Peel references, arrays, etc. Only handle Path types let mut t = ty; if let Type::Reference(r) = t { t = &*r.elem; } if let Type::Path(p) = t { // Handle Option, Result, etc. if let Some(seg) = p.path.segments.last() { let ident = seg.ident.to_string(); if ident == "Option" { // extract generic arg (use helper) if let Some(inner) = first_type_arg_from_pathargs(&seg.arguments) { return extract_ident_from_type(inner); } } else { // For multi-segment like core::java::JavaDownloadInfo we return last segment ident return Some(ident); } } } None } fn first_type_arg_from_pathargs(pa: &PathArguments) -> Option<&Type> { // Given PathArguments (e.g. from a PathSegment), return the first GenericArgument::Type if present. if let PathArguments::AngleBracketed(ab) = pa { for arg in ab.args.iter() { if let syn::GenericArgument::Type(ty) = arg { return Some(ty); } } } None } fn rust_type_to_ts(ty: &Type) -> (String, bool) { // returns (ts_type, is_struct_like) // is_struct_like signals that this type probably needs import from `import_from` // Simple mapping: String -> string, primitives -> number, bool -> boolean, others -> ident let mut t = ty; // Unwrap references if let Type::Reference(r) = t { t = &*r.elem; } if let Type::Path(p) = t { if let Some(seg) = p.path.segments.last() { let ident = seg.ident.to_string(); // handle Option if ident == "Option" { if let Some(inner) = first_type_arg_from_pathargs(&seg.arguments) { let (inner_ts, inner_struct) = rust_type_to_ts(inner); // make optional, represent as type | null return (format!("{} | null", inner_ts), inner_struct); } } return match ident.as_str() { "String" => ("string".to_string(), false), "str" => ("string".to_string(), false), "bool" => ("boolean".to_string(), false), "u8" | "u16" | "u32" | "u64" | "u128" | "i8" | "i16" | "i32" | "i64" | "i128" | "usize" | "isize" | "f32" | "f64" => ("number".to_string(), false), "Vec" => { // Vec -> T[] if let Some(inner) = first_type_arg_from_pathargs(&seg.arguments) { let (inner_ts, inner_struct) = rust_type_to_ts(inner); return (format!("{}[]", inner_ts), inner_struct); } ("unknown[]".to_string(), false) } other => { // treat as struct/complex type (other.to_string(), true) } }; } } ("unknown".to_string(), false) } fn get_return_ts(ty: &ReturnType) -> (String, BTreeSet) { // returns (promise_ts_type, set_of_structs_to_import) let mut imports = BTreeSet::new(); match ty { ReturnType::Default => ("Promise".to_string(), imports), ReturnType::Type(_, boxed) => { // look for Result commonly let t = &**boxed; if let Type::Path(p) = t { if let Some(seg) = p.path.segments.last() { let ident = seg.ident.to_string(); if ident == "Result" { if let Some(ok_ty) = first_type_arg_from_pathargs(&seg.arguments) { let (ts, is_struct) = rust_type_to_ts(ok_ty); if is_struct { if let Some(name) = extract_ident_from_type(ok_ty) { imports.insert(name); } } return (format!("Promise<{}>", ts), imports); } } else { // not Result - map directly let (ts, is_struct) = rust_type_to_ts(t); if is_struct { if let Some(name) = extract_ident_from_type(t) { imports.insert(name); } } return (format!("Promise<{}>", ts), imports); } } } // fallback ("Promise".to_string(), imports) } } } #[proc_macro_attribute] pub fn api(attr: TokenStream, item: TokenStream) -> TokenStream { // Parse attribute args via `darling` crate let meta: MacroArgs = match syn::parse(attr) { Ok(meta) => meta, Err(err) => return err.into_compile_error().into(), }; let input_fn = parse_macro_input!(item as ItemFn); // Extract attribute args: export_to, import_from let export_to = match (meta.export_to, meta.export_to_path) { (Some(to), None) => Some(to), (_, Some(path)) => Some(path), (None, None) => None, }; let import_from = meta.import_from; // Analyze function let fn_name_ident: Ident = input_fn.sig.ident.clone(); let fn_name = fn_name_ident.to_string(); let ts_fn_name = fn_name.to_lower_camel_case(); // Collect parameters (ignore State/Window) let mut param_names: Vec = Vec::new(); let mut param_defs: Vec = Vec::new(); let mut import_types: BTreeSet = BTreeSet::new(); for input in input_fn.sig.inputs.iter() { match input { FnArg::Receiver(_) => { // skip self } FnArg::Typed(pt) => { // Get parameter identifier let pat = &*pt.pat; let param_ident = if let Pat::Ident(pi) = pat { Some(pi.ident.to_string()) } else { // ignore complex patterns continue; }; // Check if type should be ignored (State, Window) if is_tauri_native(&*pt.ty) { continue; } // Map type let (ts_type, is_struct) = rust_type_to_ts(&*pt.ty); if is_struct { if let Some(name) = extract_ident_from_type(&*pt.ty) { import_types.insert(name); } } if let Some(pn) = param_ident { // Convert param name to camelCase - keep existing but ensure camelCase for TS // We'll convert snake_case param names to camelCase let ts_param_name = pn.to_lower_camel_case(); param_names.push(ts_param_name.clone()); param_defs.push(format!("{}: {}", ts_param_name, ts_type)); } } } } // Return type let (return_ts_promise, return_imports) = get_return_ts(&input_fn.sig.output); for r in return_imports { import_types.insert(r); } // Build TypeScript code string // let mut ts_lines: Vec = Vec::new(); // ts_lines.push(r#"import { invoke } from "@tauri-apps/api/core""#.to_string()); // if !import_types.is_empty() { // if let Some(import_from_str) = import_from.clone() { // let types_joined = import_types.iter().cloned().collect::>().join(", "); // ts_lines.push(format!( // "import {{ {} }} from \"{}\"", // types_joined, import_from_str // )); // } else { // // If no import_from provided, still import types from local path? We'll skip if not provided. // } // } // function signature let params_sig = param_defs.join(", "); let params_pass = if param_names.is_empty() { "".to_string() } else { // Build object like { majorVersion, imageType } format!("{}", param_names.join(", ")) }; // Determine return generic for invoke: need the raw type (not Promise<...>) let invoke_generic = if return_ts_promise.starts_with("Promise<") && return_ts_promise.ends_with('>') { &return_ts_promise["Promise<".len()..return_ts_promise.len() - 1] } else { "unknown" }; let invoke_line = if param_names.is_empty() { format!(" return invoke<{}>(\"{}\")", invoke_generic, fn_name) } else { format!( " return invoke<{}>(\"{}\", {{ {} }})", invoke_generic, fn_name, params_pass ) }; // ts_lines.push(format!( // "export async function {}({}): {} {{", // ts_fn_name, params_sig, return_ts_promise // )); // ts_lines.push(invoke_line); // ts_lines.push("}".to_string()); // let ts_contents = ts_lines.join("\n") + "\n"; // Prepare test function name let test_fn_name = Ident::new( &format!("tauri_export_bindings_{}", fn_name), fn_name_ident.span(), ); // Generate code for test function that writes the TS string to file let export_to_literal = match export_to { Some(ref s) => s.clone(), None => String::new(), }; // Build tokens let original_fn = &input_fn; // let ts_string_literal = ts_contents.clone(); // let write_stmt = if export_to_literal.is_empty() { // // No-op: don't write // // quote! { // // // No export_to provided; skipping file write. // // } // panic!("No export_to provided") // } else { // // We'll append to the file to avoid overwriting existing bindings from other macros. // // Use create(true).append(true) // let path = export_to_literal.clone(); // let ts_lit = syn::LitStr::new(&ts_string_literal, proc_macro2::Span::call_site()); // quote! { // { // // Ensure parent directories exist if possible (best-effort) // let path = std::path::Path::new(#path); // if let Some(parent) = path.parent() { // let _ = std::fs::create_dir_all(parent); // } // // Append generated bindings to file // match OpenOptions::new().create(true).append(true).open(path) { // Ok(mut f) => { // let _ = f.write_all(#ts_lit.as_bytes()); // println!("Successfully wrote to {}", path.display()); // } // Err(e) => { // eprintln!("dropout::api binding write failed: {}", e); // } // } // } // } // }; let register_stmt = quote! { ::dropout_core::inventory::submit! { ::dropout_core::ApiInfo { fn_name: #fn_name, ts_fn_name: #ts_fn_name, param_names: vec![#(#param_names),*], param_defs: vec![#(#param_defs),*], return_ts_promise: #return_ts_promise, import_types: BTreeSet::from([#(#import_types),*]), import_from: #import_from, export_to: #export_to_literal, } } }; let gen = quote! { #original_fn #[cfg(test)] mod __dropout_export_tests { use super::*; use std::fs::OpenOptions; use std::io::Write; #[test] fn #test_fn_name() { // Generated TypeScript bindings for function: #fn_name // #write_stmt #register_stmt } } }; gen.into() }