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> {
54 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok()?;
55 let cargo_toml_path = std::path::Path::new(&manifest_dir).join("Cargo.toml");
56 let content = std::fs::read_to_string(cargo_toml_path).ok()?;
57 let manifest: toml::Value = toml::from_str(&content).ok()?;
58
59 let client_path = manifest
60 .get("package")?
61 .get("metadata")?
62 .get("rorpc")?
63 .get("client_path")?
64 .as_str()?;
65
66 let resolved = std::path::Path::new(&manifest_dir).join(client_path);
68
69 let absolute = resolved
72 .canonicalize()
73 .unwrap_or(resolved)
74 .to_string_lossy()
75 .into_owned();
76
77 Some(absolute)
78}
79
80pub fn expand_contract(args: ContractArgs, func: ItemFn) -> TokenStream {
94 let ItemFn {
95 attrs,
96 vis,
97 sig,
98 block,
99 ..
100 } = func;
101
102 let original_body = &block.stmts;
103
104 let path_tokens: TokenStream = if let Some(expr) = args.path_expr {
109 quote! { #expr }
110 } else if let Some(path) = read_metadata_client_path() {
111 quote! { #path }
113 } else {
114 quote! { env!("RORPC_CLIENT_PATH") }
115 };
116
117 quote! {
118 #(#attrs)*
119 #vis #sig {
120 #[cfg(debug_assertions)]
121 {
122 ::rorpc::generate_contract()
123 .output(#path_tokens)
124 .expect("contract generation failed");
125 }
126
127 #(#original_body)*
128 }
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use quote::quote;
136
137 #[test]
138 fn parse_empty_args() {
139 let args: ContractArgs = syn::parse2(quote! {}).expect("parse failed");
140 assert!(args.path_expr.is_none());
141 }
142
143 #[test]
144 fn parse_string_literal() {
145 let args: ContractArgs = syn::parse2(quote! { "../client/bindings.ts" })
146 .expect("parse failed");
147 assert!(args.path_expr.is_some());
148 }
149
150 #[test]
151 fn parse_env_macro() {
152 let args: ContractArgs = syn::parse2(quote! { env!("RORPC_CLIENT_PATH") })
153 .expect("parse failed");
154 assert!(args.path_expr.is_some());
155 }
156
157 #[test]
158 fn parse_concat_macro() {
159 let args: ContractArgs = syn::parse2(quote! {
160 concat!(env!("CARGO_MANIFEST_DIR"), "/../client/src/rpc/bindings.ts")
161 })
162 .expect("parse failed");
163 assert!(args.path_expr.is_some());
164 }
165
166 #[test]
167 fn parse_constant() {
168 let args: ContractArgs = syn::parse2(quote! { CLIENT_PATH }).expect("parse failed");
169 assert!(args.path_expr.is_some());
170 }
171
172 #[test]
173 fn expand_with_string_literal() {
174 let func: ItemFn = syn::parse2(quote! {
175 fn main() { println!("Hello"); }
176 })
177 .expect("parse failed");
178
179 let args: ContractArgs = syn::parse2(quote! { "../client/bindings.ts" })
180 .expect("parse failed");
181 let expanded = expand_contract(args, func);
182 let s = expanded.to_string();
183
184 assert!(s.contains("\"../client/bindings.ts\""));
185 assert!(s.contains("rorpc :: generate_contract"));
186 assert!(s.contains("# [cfg (debug_assertions)]") || s.contains("#[cfg(debug_assertions)]"));
188 }
189
190 #[test]
191 fn expand_preserves_attributes() {
192 let func: ItemFn = syn::parse2(quote! {
193 #[tokio::main]
194 async fn main() { println!("Hello"); }
195 })
196 .expect("parse failed");
197
198 let args: ContractArgs = syn::parse2(quote! { "../client/bindings.ts" })
199 .expect("parse failed");
200 let expanded = expand_contract(args, func);
201 let s = expanded.to_string();
202
203 assert!(s.contains("# [tokio :: main]") || s.contains("#[tokio::main]"));
205 assert!(s.contains("async fn main"));
206 }
207}