scopes_macros/
lib.rs

1#![warn(missing_docs)]
2//! Macros for [`scopes-rs`](https://github.com/aripot007/scopes-rs)
3//!
4//! This crate is re-exported by `scopes-rs`, and its documentation should be
5//! browsed from the `scopes-rs` crate.
6//!  
7extern crate proc_macro2;
8
9use std::collections::HashMap;
10
11use darling::{FromDeriveInput, FromVariant, ast};
12use proc_macro::TokenStream;
13use proc_macro2::Span;
14use quote::quote;
15
16use crate::scope::Scope;
17
18#[cfg(feature = "hierarchy")]
19mod hierarchy;
20
21mod scope;
22
23// Options for the enum to be derived
24#[derive(FromDeriveInput)]
25#[darling(attributes(scope), supports(enum_unit))]
26struct ScopeOpts {
27    ident: syn::Ident,
28
29    #[darling(default = || ".".to_string())]
30    separator: String,
31
32    #[darling(default)]
33    prefix: String,
34
35    #[cfg(feature = "hierarchy")]
36    #[darling(default = || true)]
37    hierarchy: bool,
38
39    data: ast::Data<ScopeVariantOpts, ()>,
40}
41
42// Options for an enum variant of the scope enum
43#[derive(Debug, FromVariant)]
44#[darling(attributes(scope))]
45struct ScopeVariantOpts {
46    ident: syn::Ident,
47
48    rename: Option<String>,
49}
50
51/// ## Optional `#[scope(...)]` attributes for the enum
52/// 
53/// - `separator = "..."`: Change the separator between scope labels. Defaults to `"."`
54/// - `prefix = "..."`: Add a prefix to every generated scope name. Default is an empty prefix
55/// - `hierarchy = bool`: Enable or disable generation of the `Hierarchized` trait. Requires the `hierarchy`
56///     feature. Defaults to `true`.
57/// 
58/// ## Optional `#[scope(...)]` attributes for enum variants
59/// 
60/// - `rename = "..."`: Use a specific name instead of inferring it from the variant name
61/// 
62#[proc_macro_derive(Scope, attributes(scope))]
63pub fn derive_into_scope(item: TokenStream) -> TokenStream {
64    
65    let input = syn::parse_macro_input!(item as syn::DeriveInput);
66
67    let opts = match ScopeOpts::from_derive_input(&input) {
68        Ok(opts) => opts,
69        Err(err) => {
70            return err.write_errors().into()
71        },
72    };
73    
74    derive_into_scope_impl(&opts)
75}
76
77fn derive_into_scope_impl(opts: &ScopeOpts) -> TokenStream {
78
79    let enum_ident = &opts.ident;
80
81    // Extract enum variants and their options
82    let variants = match &opts.data {
83        ast::Data::Enum(items) => items,
84        ast::Data::Struct(_) => {
85            return syn::Error::new(Span::call_site(), "The Scope derive macro only accepts enums").into_compile_error().into();
86        },
87    };
88
89    // Parse scope names
90    let mut scopes: HashMap<String, Scope> = HashMap::with_capacity(variants.len());
91    let mut error: Option<syn::Error> = None;
92
93    for variant in variants {
94        
95        let scope = Scope::from_variant(variant, &opts);
96        let scope_full_name = scope.full_name();
97
98        // Raise error for scopes with conflicting names
99        if let Some(other_scope) = scopes.get(&scope_full_name) {
100            let mut err = syn::Error::new(
101                variant.ident.span(), 
102                format!("Conflicting scope name '{}' (conflicting with variant {}::{})", scope.name(), enum_ident, &other_scope.ident)
103            );
104
105            err.combine(syn::Error::new(
106                other_scope.ident.span(), 
107                format!("Conflicting scope name '{}' (conflicting with variant {}::{})", other_scope.name(), enum_ident, variant.ident)
108            ));
109
110            if let Some(error) = error.as_mut() {
111                error.combine(err);
112            } else {
113                error = Some(err)
114            }
115
116        } else {
117            scopes.insert(scope_full_name, scope);
118        }
119
120    }
121
122    if let Some(err) = error {
123        return err.into_compile_error().into();
124    }
125
126    // Implement parsing from a string
127
128    let (scopes_full_names, scopes_ident): (Vec<_>, Vec<_>) = scopes
129        .iter()
130        .map(|(k, v)| (k, &v.ident))
131        .unzip();
132
133    let fromstr_impl = quote! {
134        impl ::std::str::FromStr for #enum_ident {
135            type Err = ::scopes_rs::error::ScopeParseError;
136
137            fn from_str(s: &str) -> ::std::result::Result<Self, Self::Err> {
138                match s {
139                    #(#scopes_full_names => Ok(#enum_ident::#scopes_ident),)*
140                    _ => Err(::scopes_rs::error::ScopeParseError(s.to_string())),
141                }
142            }
143        }
144    };
145
146    let scope_impl = quote! {
147        impl ::scopes_rs::scope::Scope for #enum_ident {}
148    };
149
150    let scope_impl = quote! {
151        #fromstr_impl
152        #scope_impl
153    };
154
155    #[cfg(feature = "hierarchy")]
156    let mut scope_impl = scope_impl;
157
158    // Add Hierarchy implementation if the feature is enabled
159    #[cfg(feature = "hierarchy")]
160    if opts.hierarchy {
161        use quote::TokenStreamExt;
162
163        use crate::hierarchy::implement_hierarchized;
164
165        scope_impl.append_all(implement_hierarchized(enum_ident, &scopes));
166    }
167
168    scope_impl.into()
169}