opentalk_kustos_prefix_impl/
lib.rs1use proc_macro::TokenStream;
6use proc_macro_crate::{FoundCrate, crate_name};
7use proc_macro2::Span;
8use quote::quote;
9
10const ATTRIBUTE_NAME: &str = "kustos_prefix";
11
12#[proc_macro_derive(KustosPrefix, attributes(kustos_prefix))]
13pub fn derive_kustos_prefix(input: TokenStream) -> TokenStream {
14 let ast = syn::parse_macro_input!(input as syn::DeriveInput);
15
16 match try_derive_kustos_prefix(ast) {
17 Ok(k) => k,
18 Err(err) => TokenStream::from(err.to_compile_error()),
19 }
20}
21
22fn try_derive_kustos_prefix(ast: syn::DeriveInput) -> Result<TokenStream, syn::Error> {
23 let kustos_prefix = crate_name("opentalk-kustos-prefix").map_err(|_| {
24 syn::Error::new(
25 Span::call_site(),
26 "Couldn't find opentalk-kustos-prefix crate",
27 )
28 })?;
29
30 let reexports = match kustos_prefix {
31 FoundCrate::Itself => quote!(crate::__exports),
32 FoundCrate::Name(name) => {
33 let ident = proc_macro2::Ident::new(&name, Span::call_site());
34 quote!(#ident::__exports)
35 }
36 };
37
38 let msg = "#[derive(KustosPrefix)] can only be used on anonymous structs with a single field.";
39
40 let syn::Data::Struct(data_struct) = ast.data else {
41 return Err(syn::Error::new(Span::call_site(), msg));
42 };
43
44 let syn::Fields::Unnamed(fields) = data_struct.fields else {
45 return Err(syn::Error::new(Span::call_site(), msg));
46 };
47
48 if fields.unnamed.len() != 1 {
49 return Err(syn::Error::new(Span::call_site(), msg));
50 }
51
52 let ident = ast.ident;
53 let kustos_prefix = get_prefix_from_attributes(&ast.attrs)?;
54
55 let expanded = quote! {
56 impl #reexports::kustos_shared::resource::Resource for #ident {
57 const PREFIX: &'static str = #kustos_prefix;
58 }
59 };
60
61 Ok(TokenStream::from(expanded))
62}
63
64fn get_prefix_from_attributes(attrs: &[syn::Attribute]) -> Result<syn::LitStr, syn::Error> {
65 let mut found_attr = None;
66 for attr in attrs {
67 if let Some(segment) = attr.path().segments.iter().next() {
68 if segment.ident == ATTRIBUTE_NAME {
69 if found_attr.is_some() {
70 return Err(syn::Error::new(
71 Span::call_site(),
72 format!("Multiple #[{ATTRIBUTE_NAME}(...)] found"),
73 ));
74 } else {
75 found_attr = Some(attr);
76 }
77 }
78 }
79 }
80
81 if let Some(attr) = found_attr {
82 return parse_attribute(attr.meta.clone());
83 }
84
85 Err(syn::Error::new(
86 Span::call_site(),
87 format!("Attribute #[{ATTRIBUTE_NAME}(...)] missing for #[derive(KustosPrefix)]"),
88 ))
89}
90
91fn parse_attribute(meta: syn::Meta) -> Result<syn::LitStr, syn::Error> {
92 match meta {
93 syn::Meta::List(syn::MetaList {
94 path: _,
95 delimiter,
96 tokens,
97 }) => {
98 if !matches!(delimiter, syn::MacroDelimiter::Paren(_)) {
99 return Err(syn::Error::new(
100 Span::call_site(),
101 format!("Attribute #[{ATTRIBUTE_NAME}(...)] requires parentheses: `(...)`"),
102 ));
103 }
104
105 syn::parse2::<syn::LitStr>(tokens)
106 }
107 syn::Meta::Path(_) | syn::Meta::NameValue(_) => Err(syn::Error::new(
108 Span::call_site(),
109 format!("Attribute #[{ATTRIBUTE_NAME}(...)] requires parentheses: `(...)`"),
110 )),
111 }
112}