Skip to main content

typesec_macro/
lib.rs

1//! # typesec-macro
2//!
3//! Procedural macros for the typesec ecosystem.
4//!
5//! ## `#[derive(TypesecRole)]`
6//!
7//! Derive the [`Role`][typesec_core::role::Role] trait for a struct, pulling
8//! permissions and resource patterns from the `#[role(...)]` attribute:
9//!
10//! ```rust,ignore
11//! use typesec_macro::TypesecRole;
12//!
13//! #[derive(TypesecRole)]
14//! #[role(permissions = "read,write", resources = "code/*,infra/*")]
15//! pub struct Engineer;
16//! ```
17//!
18//! Expands to:
19//!
20//! ```rust,ignore
21//! impl typesec_core::role::Role for Engineer {
22//!     fn name() -> &'static str { "Engineer" }
23//!     fn permission_names() -> &'static [&'static str] { &["read", "write"] }
24//!     fn resource_patterns() -> &'static [&'static str] { &["code/*", "infra/*"] }
25//! }
26//! ```
27//!
28//! ## `policy!` macro
29//!
30//! Inline role definitions without a YAML file:
31//!
32//! ```rust,ignore
33//! use typesec_macro::policy;
34//!
35//! policy! {
36//!     role Analyst {
37//!         can [read, read_sensitive] on ["reports/*", "metrics/*"];
38//!     }
39//! }
40//! ```
41
42use proc_macro::TokenStream;
43use proc_macro2::Span;
44use quote::quote;
45use syn::{DeriveInput, LitStr, parse_macro_input};
46
47/// Derive the `typesec_core::role::Role` trait.
48///
49/// Requires a `#[role(permissions = "...", resources = "...")]` attribute.
50#[proc_macro_derive(TypesecRole, attributes(role))]
51pub fn derive_typesec_role(input: TokenStream) -> TokenStream {
52    let input = parse_macro_input!(input as DeriveInput);
53    match derive_typesec_role_impl(input) {
54        Ok(ts) => ts.into(),
55        Err(e) => e.to_compile_error().into(),
56    }
57}
58
59fn derive_typesec_role_impl(input: DeriveInput) -> Result<proc_macro2::TokenStream, syn::Error> {
60    let struct_name = &input.ident;
61    let struct_name_str = struct_name.to_string().to_lowercase();
62
63    // Find the #[role(...)] attribute.
64    let role_attr = input
65        .attrs
66        .iter()
67        .find(|a| a.path().is_ident("role"))
68        .ok_or_else(|| {
69            syn::Error::new(
70                Span::call_site(),
71                "TypesecRole requires a #[role(permissions = \"...\", resources = \"...\")] attribute",
72            )
73        })?;
74
75    // Parse the key=value pairs inside the attribute.
76    let mut permissions: Vec<String> = Vec::new();
77    let mut resources: Vec<String> = Vec::new();
78
79    role_attr.parse_nested_meta(|meta| {
80        if meta.path.is_ident("permissions") {
81            let value: LitStr = meta.value()?.parse()?;
82            permissions = value
83                .value()
84                .split(',')
85                .map(|s| s.trim().to_owned())
86                .filter(|s| !s.is_empty())
87                .collect();
88            Ok(())
89        } else if meta.path.is_ident("resources") {
90            let value: LitStr = meta.value()?.parse()?;
91            resources = value
92                .value()
93                .split(',')
94                .map(|s| s.trim().to_owned())
95                .filter(|s| !s.is_empty())
96                .collect();
97            Ok(())
98        } else {
99            Err(meta.error("unknown role attribute key (expected 'permissions' or 'resources')"))
100        }
101    })?;
102
103    let perm_lits: Vec<LitStr> = permissions
104        .iter()
105        .map(|p| LitStr::new(p, Span::call_site()))
106        .collect();
107
108    let resource_lits: Vec<LitStr> = resources
109        .iter()
110        .map(|r| LitStr::new(r, Span::call_site()))
111        .collect();
112
113    let name_lit = LitStr::new(&struct_name_str, Span::call_site());
114
115    Ok(quote! {
116        impl typesec_core::role::Role for #struct_name {
117            fn name() -> &'static str {
118                #name_lit
119            }
120            fn permission_names() -> &'static [&'static str] {
121                &[#(#perm_lits),*]
122            }
123            fn resource_patterns() -> &'static [&'static str] {
124                &[#(#resource_lits),*]
125            }
126        }
127    })
128}
129
130/// Inline policy macro.
131///
132/// ```rust,ignore
133/// policy! {
134///     role Analyst {
135///         can [read, read_sensitive] on ["reports/*"];
136///     }
137///     role Engineer {
138///         can [read, write, execute] on ["code/*"];
139///     }
140/// }
141/// ```
142///
143/// Expands each `role X { ... }` block to a struct + `Role` impl.
144#[proc_macro]
145pub fn policy(input: TokenStream) -> TokenStream {
146    match policy_impl(input.into()) {
147        Ok(ts) => ts.into(),
148        Err(e) => e.to_compile_error().into(),
149    }
150}
151
152fn policy_impl(input: proc_macro2::TokenStream) -> Result<proc_macro2::TokenStream, syn::Error> {
153    use syn::{
154        Ident, Token, braced,
155        parse::{Parse, ParseStream},
156        punctuated::Punctuated,
157    };
158
159    // Mini-DSL parser for `role Name { can [perms] on ["resources"]; }` blocks.
160    struct PolicyParser(Vec<(Ident, Vec<Ident>, Vec<LitStr>)>);
161
162    impl Parse for PolicyParser {
163        fn parse(input: ParseStream) -> syn::Result<Self> {
164            let mut roles = Vec::new();
165
166            while !input.is_empty() {
167                // `role` — parse as a plain Ident (it's not a Rust keyword).
168                let kw: Ident = input.parse()?;
169                if kw != "role" {
170                    return Err(syn::Error::new(kw.span(), "expected `role`"));
171                }
172
173                // Role name
174                let name: Ident = input.parse()?;
175
176                // `{ can [perms] on ["resources"]; }`
177                let content;
178                braced!(content in input);
179
180                // `can`
181                let can_kw: Ident = content.parse()?;
182                if can_kw != "can" {
183                    return Err(syn::Error::new(can_kw.span(), "expected `can`"));
184                }
185
186                // `[perm1, perm2, ...]`
187                let perm_content;
188                syn::bracketed!(perm_content in content);
189                let perms: Punctuated<Ident, Token![,]> =
190                    perm_content.parse_terminated(Ident::parse, Token![,])?;
191
192                // `on`
193                let on_kw: Ident = content.parse()?;
194                if on_kw != "on" {
195                    return Err(syn::Error::new(on_kw.span(), "expected `on`"));
196                }
197
198                // `["resource1", ...]`
199                let res_content;
200                syn::bracketed!(res_content in content);
201                let resources: Punctuated<LitStr, Token![,]> =
202                    res_content.parse_terminated(Parse::parse, Token![,])?;
203
204                // Optional semicolon
205                let _ = content.parse::<Token![;]>();
206
207                roles.push((
208                    name,
209                    perms.into_iter().collect(),
210                    resources.into_iter().collect(),
211                ));
212            }
213
214            Ok(PolicyParser(roles))
215        }
216    }
217
218    let parsed: PolicyParser = syn::parse2(input)?;
219    let mut output = proc_macro2::TokenStream::new();
220
221    for (name, perms, resources) in parsed.0 {
222        let name_str = name.to_string().to_lowercase();
223        let perm_strs: Vec<String> = perms.iter().map(|p| p.to_string()).collect();
224        let perm_lits: Vec<LitStr> = perm_strs
225            .iter()
226            .map(|s| LitStr::new(s, Span::call_site()))
227            .collect();
228
229        let name_lit = LitStr::new(&name_str, Span::call_site());
230
231        output.extend(quote! {
232            #[derive(Debug, Clone, Copy)]
233            pub struct #name;
234
235            impl typesec_core::role::Role for #name {
236                fn name() -> &'static str { #name_lit }
237                fn permission_names() -> &'static [&'static str] { &[#(#perm_lits),*] }
238                fn resource_patterns() -> &'static [&'static str] { &[#(#resources),*] }
239            }
240        });
241    }
242
243    Ok(output)
244}