use proc_macro2::TokenStream;
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
Expr, ItemFn,
};
pub struct ContractArgs {
pub path_expr: Option<Expr>,
}
impl Parse for ContractArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
if input.is_empty() {
return Ok(ContractArgs { path_expr: None });
}
let expr: Expr = input.parse()?;
Ok(ContractArgs {
path_expr: Some(expr),
})
}
}
fn read_metadata_client_path() -> Option<String> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok()?;
let cargo_toml_path = std::path::Path::new(&manifest_dir).join("Cargo.toml");
let content = std::fs::read_to_string(cargo_toml_path).ok()?;
let manifest: toml::Value = toml::from_str(&content).ok()?;
let client_path = manifest
.get("package")?
.get("metadata")?
.get("rorpc")?
.get("client_path")?
.as_str()?;
let resolved = std::path::Path::new(&manifest_dir).join(client_path);
let absolute = resolved
.canonicalize()
.unwrap_or(resolved)
.to_string_lossy()
.into_owned();
Some(absolute)
}
pub fn expand_contract(args: ContractArgs, func: ItemFn) -> TokenStream {
let ItemFn {
attrs,
vis,
sig,
block,
..
} = func;
let original_body = &block.stmts;
let path_tokens: TokenStream = if let Some(expr) = args.path_expr {
quote! { #expr }
} else if let Some(path) = read_metadata_client_path() {
quote! { #path }
} else {
quote! { env!("RORPC_CLIENT_PATH") }
};
quote! {
#(#attrs)*
#vis #sig {
#[cfg(debug_assertions)]
{
::rorpc::generate_contract()
.output(#path_tokens)
.expect("contract generation failed");
}
#(#original_body)*
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
#[test]
fn parse_empty_args() {
let args: ContractArgs = syn::parse2(quote! {}).expect("parse failed");
assert!(args.path_expr.is_none());
}
#[test]
fn parse_string_literal() {
let args: ContractArgs = syn::parse2(quote! { "../client/bindings.ts" })
.expect("parse failed");
assert!(args.path_expr.is_some());
}
#[test]
fn parse_env_macro() {
let args: ContractArgs = syn::parse2(quote! { env!("RORPC_CLIENT_PATH") })
.expect("parse failed");
assert!(args.path_expr.is_some());
}
#[test]
fn parse_concat_macro() {
let args: ContractArgs = syn::parse2(quote! {
concat!(env!("CARGO_MANIFEST_DIR"), "/../client/src/rpc/bindings.ts")
})
.expect("parse failed");
assert!(args.path_expr.is_some());
}
#[test]
fn parse_constant() {
let args: ContractArgs = syn::parse2(quote! { CLIENT_PATH }).expect("parse failed");
assert!(args.path_expr.is_some());
}
#[test]
fn expand_with_string_literal() {
let func: ItemFn = syn::parse2(quote! {
fn main() { println!("Hello"); }
})
.expect("parse failed");
let args: ContractArgs = syn::parse2(quote! { "../client/bindings.ts" })
.expect("parse failed");
let expanded = expand_contract(args, func);
let s = expanded.to_string();
assert!(s.contains("\"../client/bindings.ts\""));
assert!(s.contains("rorpc :: generate_contract"));
assert!(s.contains("# [cfg (debug_assertions)]") || s.contains("#[cfg(debug_assertions)]"));
}
#[test]
fn expand_preserves_attributes() {
let func: ItemFn = syn::parse2(quote! {
#[tokio::main]
async fn main() { println!("Hello"); }
})
.expect("parse failed");
let args: ContractArgs = syn::parse2(quote! { "../client/bindings.ts" })
.expect("parse failed");
let expanded = expand_contract(args, func);
let s = expanded.to_string();
assert!(s.contains("# [tokio :: main]") || s.contains("#[tokio::main]"));
assert!(s.contains("async fn main"));
}
}