Skip to main content

rorpc_parse/codegen/
namespace.rs

1//! Code generation for the `#[rorpc::namespace("/prefix")]` attribute macro.
2//!
3//! Registers a namespace prefix for a module. All handlers within the module
4//! will have their paths prefixed with this namespace at runtime (during router
5//! construction and contract generation).
6
7use proc_macro2::TokenStream;
8use quote::quote;
9use syn::{
10    parse::{Parse, ParseStream},
11    Item, LitStr,
12};
13
14use crate::errors::Result;
15
16/// Parsed arguments for the `#[rorpc::namespace("/prefix")]` attribute.
17#[derive(Debug)]
18pub struct NamespaceArgs {
19    pub prefix: String,
20}
21
22impl Parse for NamespaceArgs {
23    fn parse(input: ParseStream) -> syn::Result<Self> {
24        let lit: LitStr = input.parse()?;
25        let prefix = lit.value();
26
27        // Validate prefix
28        if !prefix.starts_with('/') {
29            return Err(syn::Error::new(
30                lit.span(),
31                "namespace prefix must start with '/'",
32            ));
33        }
34
35        if prefix.contains("..") {
36            return Err(syn::Error::new(
37                lit.span(),
38                "namespace prefix cannot contain '..' path traversal",
39            ));
40        }
41
42        if prefix.ends_with('/') && prefix.len() > 1 {
43            return Err(syn::Error::new(
44                lit.span(),
45                "namespace prefix should not end with '/' (except for root)",
46            ));
47        }
48
49        Ok(NamespaceArgs { prefix })
50    }
51}
52
53/// Expand the `#[rorpc::namespace("/prefix")]` attribute.
54///
55/// Returns the original item unchanged plus an `inventory::submit!` registration
56/// for `NamespaceMetadata`.
57pub fn expand_namespace(args: NamespaceArgs, item: Item) -> Result<TokenStream> {
58    let prefix = &args.prefix;
59
60    Ok(quote! {
61        #item
62
63        ::rorpc::inventory::submit! {
64            ::rorpc::NamespaceMetadata {
65                module_path: ::std::module_path!(),
66                prefix: #prefix,
67            }
68        }
69    })
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn parse_valid_prefix() {
78        let args: NamespaceArgs = syn::parse_str("\"/planet\"").unwrap();
79        assert_eq!(args.prefix, "/planet");
80    }
81
82    #[test]
83    fn parse_root_prefix() {
84        let args: NamespaceArgs = syn::parse_str("\"/\"").unwrap();
85        assert_eq!(args.prefix, "/");
86    }
87
88    #[test]
89    fn reject_missing_leading_slash() {
90        let result: syn::Result<NamespaceArgs> = syn::parse_str("\"planet\"");
91        assert!(result.is_err());
92        assert!(result
93            .unwrap_err()
94            .to_string()
95            .contains("must start with '/'"));
96    }
97
98    #[test]
99    fn reject_path_traversal() {
100        let result: syn::Result<NamespaceArgs> = syn::parse_str("\"/planet/../admin\"");
101        assert!(result.is_err());
102        assert!(result.unwrap_err().to_string().contains(".."));
103    }
104
105    #[test]
106    fn reject_trailing_slash() {
107        let result: syn::Result<NamespaceArgs> = syn::parse_str("\"/planet/\"");
108        assert!(result.is_err());
109        assert!(result
110            .unwrap_err()
111            .to_string()
112            .contains("should not end with '/'"));
113    }
114}