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 normalize_path(path: &std::path::Path) -> std::path::PathBuf {
55 use std::path::Component;
56
57 let mut stack: Vec<std::ffi::OsString> = Vec::new();
58
59 for component in path.components() {
60 match component {
61 Component::Prefix(_) => {
62 stack.clear();
64 stack.push(component.as_os_str().to_owned());
65 }
66 Component::RootDir => {
67 stack.push(component.as_os_str().to_owned());
69 }
70 Component::CurDir => {
71 }
73 Component::ParentDir => {
74 let last_is_normal = stack.last().map(|s| {
77 let p = std::path::Path::new(s);
78 matches!(p.components().next(), Some(Component::Normal(_)))
79 }).unwrap_or(false);
80 if last_is_normal {
81 stack.pop();
82 }
83 }
84 Component::Normal(name) => {
85 stack.push(name.to_owned());
86 }
87 }
88 }
89
90 stack.iter().collect()
91}
92
93fn read_metadata_client_path() -> Option<String> {
105 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok()?;
106 let cargo_toml_path = std::path::Path::new(&manifest_dir).join("Cargo.toml");
107 let content = std::fs::read_to_string(cargo_toml_path).ok()?;
108 let manifest: toml::Value = toml::from_str(&content).ok()?;
109
110 let client_path = manifest
111 .get("package")
112 .and_then(|p| p.get("metadata"))
113 .and_then(|m| m.get("rorpc"))
114 .and_then(|r| r.get("client_path"))
115 .and_then(|v| v.as_str())?;
116
117 let joined = std::path::Path::new(&manifest_dir).join(client_path);
120 let absolute = normalize_path(&joined).to_string_lossy().into_owned();
121
122 Some(absolute)
123}
124
125pub fn expand_contract(args: ContractArgs, func: ItemFn) -> TokenStream {
139 let ItemFn {
140 attrs,
141 vis,
142 sig,
143 block,
144 ..
145 } = func;
146
147 let original_body = &block.stmts;
148
149 let path_tokens: TokenStream = if let Some(expr) = args.path_expr {
154 quote! { #expr }
155 } else if let Some(path) = read_metadata_client_path() {
156 quote! { #path }
158 } else {
159 quote! { env!("RORPC_CLIENT_PATH") }
160 };
161
162 quote! {
163 #(#attrs)*
164 #vis #sig {
165 #[cfg(debug_assertions)]
166 {
167 ::rorpc::generate_contract()
168 .output(#path_tokens)
169 .expect("contract generation failed");
170 }
171
172 #(#original_body)*
173 }
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use quote::quote;
181
182 #[test]
185 fn parse_empty_args() {
186 let args: ContractArgs = syn::parse2(quote! {}).expect("parse failed");
187 assert!(args.path_expr.is_none());
188 }
189
190 #[test]
191 fn parse_string_literal() {
192 let args: ContractArgs = syn::parse2(quote! { "../client/bindings.ts" })
193 .expect("parse failed");
194 assert!(args.path_expr.is_some());
195 }
196
197 #[test]
198 fn parse_env_macro() {
199 let args: ContractArgs = syn::parse2(quote! { env!("RORPC_CLIENT_PATH") })
200 .expect("parse failed");
201 assert!(args.path_expr.is_some());
202 }
203
204 #[test]
205 fn parse_concat_macro() {
206 let args: ContractArgs = syn::parse2(quote! {
207 concat!(env!("CARGO_MANIFEST_DIR"), "/../client/src/rpc/bindings.ts")
208 })
209 .expect("parse failed");
210 assert!(args.path_expr.is_some());
211 }
212
213 #[test]
214 fn parse_constant() {
215 let args: ContractArgs = syn::parse2(quote! { CLIENT_PATH }).expect("parse failed");
216 assert!(args.path_expr.is_some());
217 }
218
219 #[test]
222 fn expand_with_string_literal() {
223 let func: ItemFn = syn::parse2(quote! {
224 fn main() { println!("Hello"); }
225 })
226 .expect("parse failed");
227
228 let args: ContractArgs = syn::parse2(quote! { "../client/bindings.ts" })
229 .expect("parse failed");
230 let expanded = expand_contract(args, func);
231 let s = expanded.to_string();
232
233 assert!(s.contains("\"../client/bindings.ts\""));
234 assert!(s.contains("rorpc :: generate_contract"));
235 assert!(s.contains("# [cfg (debug_assertions)]") || s.contains("#[cfg(debug_assertions)]"));
236 }
237
238 #[test]
239 fn expand_preserves_attributes() {
240 let func: ItemFn = syn::parse2(quote! {
241 #[tokio::main]
242 async fn main() { println!("Hello"); }
243 })
244 .expect("parse failed");
245
246 let args: ContractArgs = syn::parse2(quote! { "../client/bindings.ts" })
247 .expect("parse failed");
248 let expanded = expand_contract(args, func);
249 let s = expanded.to_string();
250
251 assert!(s.contains("# [tokio :: main]") || s.contains("#[tokio::main]"));
252 assert!(s.contains("async fn main"));
253 }
254
255 #[test]
258 fn normalize_simple_parent_traversal() {
259 let p = std::path::Path::new("/repo/server/crate").join("../../out.ts");
261 assert_eq!(normalize_path(&p), std::path::Path::new("/repo/out.ts"));
262 }
263
264 #[test]
265 fn normalize_sibling_dir() {
266 let p = std::path::Path::new("/repo/server").join("../client/src/bindings.ts");
268 assert_eq!(normalize_path(&p), std::path::Path::new("/repo/client/src/bindings.ts"));
269 }
270
271 #[test]
272 fn normalize_deep_traversal_stops_at_root() {
273 let p = std::path::Path::new("/a/b").join("../../../../out.ts");
275 assert_eq!(normalize_path(&p), std::path::Path::new("/out.ts"));
276 }
277
278 #[test]
279 fn normalize_curdirs_are_skipped() {
280 let p = std::path::Path::new("/repo/./server/./crate").join("./out.ts");
281 assert_eq!(normalize_path(&p), std::path::Path::new("/repo/server/crate/out.ts"));
282 }
283
284 #[test]
285 fn normalize_already_clean_path_unchanged() {
286 let p = std::path::Path::new("/repo/client/src/bindings.ts");
287 assert_eq!(normalize_path(p), p);
288 }
289
290 #[cfg(windows)]
293 #[test]
294 fn normalize_windows_preserves_drive_letter_shallow() {
295 let base = std::path::Path::new(
299 r"D:\programming\Rust\rust-orpc\examples\axum-react\better-auth-integration",
300 );
301 let p = base.join("../../client/src/rpc/bindings.ts");
302 assert_eq!(
303 normalize_path(&p),
304 std::path::Path::new(
305 r"D:\programming\Rust\rust-orpc\examples\axum-react\client\src\rpc\bindings.ts"
306 ),
307 );
308 }
309
310 #[cfg(windows)]
311 #[test]
312 fn normalize_windows_deep_traversal_to_near_root() {
313 let base = std::path::Path::new(
316 r"D:\programming\Rust\rust-orpc\examples\axum-react\better-auth-integration",
317 );
318 let p = base.join("../../../../../out.ts");
319 assert_eq!(
320 normalize_path(&p),
321 std::path::Path::new(r"D:\programming\out.ts"),
322 );
323 }
324
325 #[cfg(windows)]
326 #[test]
327 fn normalize_windows_excessive_traversal_stops_at_root() {
328 let base = std::path::Path::new(r"D:\a\b");
330 let p = base.join("../../../../../out.ts");
331 assert_eq!(normalize_path(&p), std::path::Path::new(r"D:\out.ts"));
332 }
333
334 #[cfg(windows)]
335 #[test]
336 fn normalize_windows_no_traversal_unchanged() {
337 let p = std::path::Path::new(r"D:\programming\Rust\client\src\bindings.ts");
338 assert_eq!(normalize_path(p), p);
339 }
340}