rorpc_parse/codegen/
contract_attr.rs1use proc_macro2::TokenStream;
12use quote::quote;
13use syn::{
14 parse::{Parse, ParseStream},
15 Expr, ItemFn,
16};
17
18pub struct ContractArgs {
28 pub path_expr: Option<Expr>,
31}
32
33impl Parse for ContractArgs {
34 fn parse(input: ParseStream) -> syn::Result<Self> {
35 if input.is_empty() {
36 return Ok(ContractArgs { path_expr: None });
37 }
38 let expr: Expr = input.parse()?;
39 Ok(ContractArgs {
40 path_expr: Some(expr),
41 })
42 }
43}
44
45fn read_metadata_client_path() -> Option<String> {
51 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok()?;
52 let cargo_toml_path = std::path::Path::new(&manifest_dir).join("Cargo.toml");
53 let content = std::fs::read_to_string(cargo_toml_path).ok()?;
54 let manifest: toml::Value = toml::from_str(&content).ok()?;
55
56 let client_path = manifest
57 .get("package")?
58 .get("metadata")?
59 .get("rorpc")?
60 .get("client_path")?
61 .as_str()?;
62
63 let absolute = std::path::Path::new(&manifest_dir)
65 .join(client_path)
66 .to_string_lossy()
67 .into_owned();
68
69 Some(absolute)
70}
71
72pub fn expand_contract(args: ContractArgs, func: ItemFn) -> TokenStream {
86 let ItemFn {
87 attrs,
88 vis,
89 sig,
90 block,
91 ..
92 } = func;
93
94 let original_body = &block.stmts;
95
96 let path_tokens: TokenStream = if let Some(expr) = args.path_expr {
101 quote! { #expr }
102 } else if let Some(path) = read_metadata_client_path() {
103 quote! { #path }
105 } else {
106 quote! { env!("RORPC_CLIENT_PATH") }
107 };
108
109 quote! {
110 #(#attrs)*
111 #vis #sig {
112 #[cfg(debug_assertions)]
113 {
114 ::rorpc::generate_contract()
115 .output(#path_tokens)
116 .expect("contract generation failed");
117 }
118
119 #(#original_body)*
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use quote::quote;
128
129 #[test]
130 fn parse_empty_args() {
131 let args: ContractArgs = syn::parse2(quote! {}).expect("parse failed");
132 assert!(args.path_expr.is_none());
133 }
134
135 #[test]
136 fn parse_string_literal() {
137 let args: ContractArgs = syn::parse2(quote! { "../client/bindings.ts" })
138 .expect("parse failed");
139 assert!(args.path_expr.is_some());
140 }
141
142 #[test]
143 fn parse_env_macro() {
144 let args: ContractArgs = syn::parse2(quote! { env!("RORPC_CLIENT_PATH") })
145 .expect("parse failed");
146 assert!(args.path_expr.is_some());
147 }
148
149 #[test]
150 fn parse_concat_macro() {
151 let args: ContractArgs = syn::parse2(quote! {
152 concat!(env!("CARGO_MANIFEST_DIR"), "/../client/src/rpc/bindings.ts")
153 })
154 .expect("parse failed");
155 assert!(args.path_expr.is_some());
156 }
157
158 #[test]
159 fn parse_constant() {
160 let args: ContractArgs = syn::parse2(quote! { CLIENT_PATH }).expect("parse failed");
161 assert!(args.path_expr.is_some());
162 }
163
164 #[test]
165 fn expand_with_string_literal() {
166 let func: ItemFn = syn::parse2(quote! {
167 fn main() { println!("Hello"); }
168 })
169 .expect("parse failed");
170
171 let args: ContractArgs = syn::parse2(quote! { "../client/bindings.ts" })
172 .expect("parse failed");
173 let expanded = expand_contract(args, func);
174 let s = expanded.to_string();
175
176 assert!(s.contains("\"../client/bindings.ts\""));
177 assert!(s.contains("rorpc :: generate_contract"));
178 assert!(s.contains("# [cfg (debug_assertions)]") || s.contains("#[cfg(debug_assertions)]"));
180 }
181
182 #[test]
183 fn expand_preserves_attributes() {
184 let func: ItemFn = syn::parse2(quote! {
185 #[tokio::main]
186 async fn main() { println!("Hello"); }
187 })
188 .expect("parse failed");
189
190 let args: ContractArgs = syn::parse2(quote! { "../client/bindings.ts" })
191 .expect("parse failed");
192 let expanded = expand_contract(args, func);
193 let s = expanded.to_string();
194
195 assert!(s.contains("# [tokio :: main]") || s.contains("#[tokio::main]"));
197 assert!(s.contains("async fn main"));
198 }
199}