Skip to main content

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}