rorpc_parse/codegen/
namespace.rs1use proc_macro2::TokenStream;
8use quote::quote;
9use syn::{
10 parse::{Parse, ParseStream},
11 Item, LitStr,
12};
13
14use crate::errors::Result;
15
16#[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 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
53pub 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}