rorpc_parse/codegen/
contract_attr.rs1use proc_macro2::TokenStream;
12use quote::quote;
13use syn::{
14 Expr, ItemFn,
15 parse::{Parse, ParseStream},
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
77 .last()
78 .map(|s| {
79 let p = std::path::Path::new(s);
80 matches!(p.components().next(), Some(Component::Normal(_)))
81 })
82 .unwrap_or(false);
83 if last_is_normal {
84 stack.pop();
85 }
86 }
87 Component::Normal(name) => {
88 stack.push(name.to_owned());
89 }
90 }
91 }
92
93 stack.iter().collect()
94}
95
96fn read_metadata_client_path() -> Option<String> {
108 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok()?;
109 let cargo_toml_path = std::path::Path::new(&manifest_dir).join("Cargo.toml");
110 let content = std::fs::read_to_string(cargo_toml_path).ok()?;
111 let manifest: toml::Value = toml::from_str(&content).ok()?;
112
113 let client_path = manifest
114 .get("package")
115 .and_then(|p| p.get("metadata"))
116 .and_then(|m| m.get("rorpc"))
117 .and_then(|r| r.get("client_path"))
118 .and_then(|v| v.as_str())?;
119
120 let joined = std::path::Path::new(&manifest_dir).join(client_path);
123 let absolute = normalize_path(&joined).to_string_lossy().into_owned();
124
125 Some(absolute)
126}
127
128pub fn expand_contract(args: ContractArgs, func: ItemFn) -> TokenStream {
142 let ItemFn {
143 attrs,
144 vis,
145 sig,
146 block,
147 ..
148 } = func;
149
150 let original_body = &block.stmts;
151
152 let path_tokens: TokenStream = if let Some(expr) = args.path_expr {
157 quote! { #expr }
158 } else if let Some(path) = read_metadata_client_path() {
159 quote! { #path }
161 } else {
162 quote! { env!("RORPC_CLIENT_PATH") }
163 };
164
165 quote! {
166 #(#attrs)*
167 #vis #sig {
168 #[cfg(debug_assertions)]
169 {
170 ::rorpc::generate_contract()
171 .output(#path_tokens)
172 .expect("contract generation failed");
173 }
174
175 #(#original_body)*
176 }
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use quote::quote;
184
185 #[test]
188 fn parse_empty_args() {
189 let args: ContractArgs = syn::parse2(quote! {}).expect("parse failed");
190 assert!(args.path_expr.is_none());
191 }
192
193 #[test]
194 fn parse_string_literal() {
195 let args: ContractArgs =
196 syn::parse2(quote! { "../client/bindings.ts" }).expect("parse failed");
197 assert!(args.path_expr.is_some());
198 }
199
200 #[test]
201 fn parse_env_macro() {
202 let args: ContractArgs =
203 syn::parse2(quote! { env!("RORPC_CLIENT_PATH") }).expect("parse failed");
204 assert!(args.path_expr.is_some());
205 }
206
207 #[test]
208 fn parse_concat_macro() {
209 let args: ContractArgs = syn::parse2(quote! {
210 concat!(env!("CARGO_MANIFEST_DIR"), "/../client/src/rpc/bindings.ts")
211 })
212 .expect("parse failed");
213 assert!(args.path_expr.is_some());
214 }
215
216 #[test]
217 fn parse_constant() {
218 let args: ContractArgs = syn::parse2(quote! { CLIENT_PATH }).expect("parse failed");
219 assert!(args.path_expr.is_some());
220 }
221
222 #[test]
225 fn expand_with_string_literal() {
226 let func: ItemFn = syn::parse2(quote! {
227 fn main() { println!("Hello"); }
228 })
229 .expect("parse failed");
230
231 let args: ContractArgs =
232 syn::parse2(quote! { "../client/bindings.ts" }).expect("parse failed");
233 let expanded = expand_contract(args, func);
234 let s = expanded.to_string();
235
236 assert!(s.contains("\"../client/bindings.ts\""));
237 assert!(s.contains("rorpc :: generate_contract"));
238 assert!(s.contains("# [cfg (debug_assertions)]") || s.contains("#[cfg(debug_assertions)]"));
239 }
240
241 #[test]
242 fn expand_preserves_attributes() {
243 let func: ItemFn = syn::parse2(quote! {
244 #[tokio::main]
245 async fn main() { println!("Hello"); }
246 })
247 .expect("parse failed");
248
249 let args: ContractArgs =
250 syn::parse2(quote! { "../client/bindings.ts" }).expect("parse failed");
251 let expanded = expand_contract(args, func);
252 let s = expanded.to_string();
253
254 assert!(s.contains("# [tokio :: main]") || s.contains("#[tokio::main]"));
255 assert!(s.contains("async fn main"));
256 }
257
258 #[test]
261 fn normalize_simple_parent_traversal() {
262 let p = std::path::Path::new("/repo/server/crate").join("../../out.ts");
264 assert_eq!(normalize_path(&p), std::path::Path::new("/repo/out.ts"));
265 }
266
267 #[test]
268 fn normalize_sibling_dir() {
269 let p = std::path::Path::new("/repo/server").join("../client/src/bindings.ts");
271 assert_eq!(
272 normalize_path(&p),
273 std::path::Path::new("/repo/client/src/bindings.ts")
274 );
275 }
276
277 #[test]
278 fn normalize_deep_traversal_stops_at_root() {
279 let p = std::path::Path::new("/a/b").join("../../../../out.ts");
281 assert_eq!(normalize_path(&p), std::path::Path::new("/out.ts"));
282 }
283
284 #[test]
285 fn normalize_curdirs_are_skipped() {
286 let p = std::path::Path::new("/repo/./server/./crate").join("./out.ts");
287 assert_eq!(
288 normalize_path(&p),
289 std::path::Path::new("/repo/server/crate/out.ts")
290 );
291 }
292
293 #[test]
294 fn normalize_already_clean_path_unchanged() {
295 let p = std::path::Path::new("/repo/client/src/bindings.ts");
296 assert_eq!(normalize_path(p), p);
297 }
298
299 #[cfg(windows)]
302 #[test]
303 fn normalize_windows_preserves_drive_letter_shallow() {
304 let base = std::path::Path::new(
308 r"D:\programming\Rust\rust-orpc\examples\axum-react\better-auth-integration",
309 );
310 let p = base.join("../client/src/rpc/bindings.ts");
311 assert_eq!(
312 normalize_path(&p),
313 std::path::Path::new(
314 r"D:\programming\Rust\rust-orpc\examples\axum-react\client\src\rpc\bindings.ts"
315 ),
316 );
317 }
318
319 #[cfg(windows)]
320 #[test]
321 fn normalize_windows_deep_traversal_to_near_root() {
322 let base = std::path::Path::new(
325 r"D:\programming\Rust\rust-orpc\examples\axum-react\better-auth-integration",
326 );
327 let p = base.join("../../../../../out.ts");
328 assert_eq!(
329 normalize_path(&p),
330 std::path::Path::new(r"D:\programming\out.ts"),
331 );
332 }
333
334 #[cfg(windows)]
335 #[test]
336 fn normalize_windows_excessive_traversal_stops_at_root() {
337 let base = std::path::Path::new(r"D:\a\b");
339 let p = base.join("../../../../../out.ts");
340 assert_eq!(normalize_path(&p), std::path::Path::new(r"D:\out.ts"));
341 }
342
343 #[cfg(windows)]
344 #[test]
345 fn normalize_windows_no_traversal_unchanged() {
346 let p = std::path::Path::new(r"D:\programming\Rust\client\src\bindings.ts");
347 assert_eq!(normalize_path(p), p);
348 }
349}