structfs_path_macro/lib.rs
1//! Proc macro for compile-time validated StructFS paths.
2//!
3//! `path!` validates string literal components against the StructFS path
4//! grammar (UAX#31 identifiers or numeric strings) at compile time.
5//! Expression arguments must be `PathComponent` values, which are validated
6//! at construction time.
7//!
8//! ```ignore
9//! // A single literal path — components validated at compile time
10//! let p = path!("users/123/name");
11//!
12//! // Component style — equivalent to the above
13//! let p = path!("users", 123, "name");
14//!
15//! // Mixed — literals validated at compile time, expressions must be
16//! // PathComponent (bare String/&str fail to compile)
17//! let name = PathComponent::try_new("alice")?;
18//! let p = path!("users", name, "profile");
19//!
20//! // Compile error:
21//! // let p = path!("users/bad-name");
22//! // ^^^^^^^^^^^^^^^ invalid character '-'
23//! ```
24
25use proc_macro::TokenStream;
26
27use quote::quote;
28use syn::punctuated::Punctuated;
29use syn::{parse_macro_input, Expr, Lit, Token};
30
31/// Build a `Path` from a mix of literal and runtime components.
32///
33/// - **String literals** are split on `/` and each component is validated
34/// at compile time against the StructFS path grammar.
35/// - **Integer literals** become numeric components (array indexing).
36/// - **Expressions** must be of type `PathComponent` (runtime-validated at
37/// construction). Bare `String`/`&str` values do not compile; validate
38/// them first with `PathComponent::try_new` or `PathComponent::encode`.
39///
40/// Returns a `structfs_core_store::Path`.
41#[proc_macro]
42pub fn path(input: TokenStream) -> TokenStream {
43 let args = parse_macro_input!(input with Punctuated::<Expr, Token![,]>::parse_terminated);
44
45 let mut component_exprs = Vec::new();
46
47 for expr in &args {
48 match expr {
49 Expr::Lit(expr_lit) => match &expr_lit.lit {
50 Lit::Str(s) => {
51 // Split on '/' like Path::parse: empty segments are
52 // ignored, so "a//b/" and "" behave identically to the
53 // runtime parser.
54 for component in s.value().split('/').filter(|c| !c.is_empty()) {
55 if let Err(msg) = structfs_path_validation::validate_component(component) {
56 return syn::Error::new(
57 s.span(),
58 format!("invalid path component '{component}': {msg}"),
59 )
60 .to_compile_error()
61 .into();
62 }
63 component_exprs.push(quote! { ::std::string::String::from(#component) });
64 }
65 }
66 Lit::Int(n) => {
67 // Numeric literals are valid components (array indexing)
68 let s = n.base10_digits();
69 if let Err(msg) = structfs_path_validation::validate_component(s) {
70 return syn::Error::new(
71 n.span(),
72 format!("invalid path component '{s}': {msg}"),
73 )
74 .to_compile_error()
75 .into();
76 }
77 component_exprs.push(quote! { ::std::string::String::from(#s) });
78 }
79 other => {
80 return syn::Error::new(
81 other.span(),
82 "expected string literal, integer literal, or PathComponent expression",
83 )
84 .to_compile_error()
85 .into();
86 }
87 },
88 other => {
89 // Runtime expression — must be a PathComponent (pre-validated).
90 // We call .validated_str(), a method only PathComponent has, so
91 // bare String/&str produce a compile error. Borrows rather than
92 // consumes, so the same component can be reused across calls.
93 component_exprs.push(quote! {
94 ::std::string::String::from((#other).validated_str())
95 });
96 }
97 }
98 }
99
100 // All components are validated: literals here at compile time,
101 // PathComponent values at their construction site. The constructor
102 // re-checks with debug_assert as a safety net.
103 quote! {
104 ::structfs_core_store::Path::from_validated_components(
105 ::std::vec![#(#component_exprs),*]
106 )
107 }
108 .into()
109}