opentalk_kustos_prefix_impl/
lib.rs

1// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
2//
3// SPDX-License-Identifier: EUPL-1.2
4
5use 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}