named_item_derive/
lib.rs

1//! named-item-derive — a derive macro for implementing `Named`, `SetName`, etc.
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{
6    parse_macro_input, DeriveInput, Data, Fields, Error as SynError,
7    LitStr,
8};
9
10/// The attribute macro to derive Named, DefaultName, SetName, etc. behaviors.
11#[proc_macro_derive(NamedItem, attributes(named_item))]
12pub fn derive_named_item(input: TokenStream) -> TokenStream {
13
14    let ast = parse_macro_input!(input as DeriveInput);
15
16    // Parse the user-provided #[named_item(...)] attributes
17    let config = match parse_named_item_attrs(&ast) {
18        Ok(cfg) => cfg,
19        Err(e) => return e.to_compile_error().into(),
20    };
21
22    // Generate the final code
23    match impl_named_item(&ast, &config) {
24        Ok(ts) => ts,
25        Err(err) => err.to_compile_error().into(),
26    }
27}
28
29/// Configuration extracted from `#[named_item(...)]`.
30struct NamedItemConfig {
31    /// Optional default name if `default_name="foo"`.
32    default_name: Option<String>,
33    /// If `aliases="true"`, the struct must have `aliases: Vec<String>`.
34    aliases: bool,
35    /// If `default_aliases="foo,bar"`, we store them here.
36    default_aliases: Vec<String>,
37    /// If `history="true"`, the struct must have `name_history: Vec<String>`.
38    history: bool,
39}
40
41fn parse_named_item_attrs(ast: &DeriveInput) -> syn::Result<NamedItemConfig> {
42    let mut default_name = None;
43    let mut aliases = false;
44    let mut default_aliases = Vec::new();
45    let mut history = false;
46
47    for attr in &ast.attrs {
48        if attr.path().is_ident("named_item") {
49            // parse_nested_meta helps parse name="value" pairs
50            attr.parse_nested_meta(|meta| {
51                let p = &meta.path;
52                if p.is_ident("default_name") {
53                    let lit: LitStr = meta.value()?.parse()?;
54                    default_name = Some(lit.value());
55                } else if p.is_ident("aliases") {
56                    let lit: LitStr = meta.value()?.parse()?;
57                    aliases = lit.value().to_lowercase() == "true";
58                } else if p.is_ident("default_aliases") {
59                    let lit: LitStr = meta.value()?.parse()?;
60                    default_aliases = lit
61                        .value()
62                        .split(',')
63                        .filter(|tok| !tok.trim().is_empty())
64                        .map(|s| s.trim().to_string())
65                        .collect();
66                } else if p.is_ident("history") {
67                    let lit: LitStr = meta.value()?.parse()?;
68                    history = lit.value().to_lowercase() == "true";
69                }
70                Ok(())
71            })?;
72        }
73    }
74
75    Ok(NamedItemConfig {
76        default_name,
77        aliases,
78        default_aliases,
79        history,
80    })
81}
82
83/// Generate the trait implementations for the given struct:
84/// - Named
85/// - DefaultName
86/// - ResetName
87/// - SetName
88/// - NameHistory (optionally)
89/// - NamedAlias (optionally)
90fn impl_named_item(ast: &DeriveInput, cfg: &NamedItemConfig) -> syn::Result<TokenStream> {
91    let struct_name = &ast.ident;
92
93    // ### 1) Capture generics
94    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
95
96    // ### 2) We only support normal "struct { name: String, ... }"
97    let fields = match &ast.data {
98        Data::Struct(ds) => &ds.fields,
99        _ => {
100            return Err(SynError::new_spanned(
101                &ast.ident,
102                "NamedItem can only be derived on a struct.",
103            ));
104        }
105    };
106
107    let named_fields = match fields {
108        Fields::Named(f) => &f.named,
109        _ => {
110            return Err(SynError::new_spanned(
111                &ast.ident,
112                "NamedItem requires a struct with named fields.",
113            ));
114        }
115    };
116
117    // Must have `name: String`
118    let name_field = named_fields.iter().find(|field| {
119        field.ident.as_ref().map(|id| id == "name").unwrap_or(false)
120    });
121    if name_field.is_none() {
122        return Err(SynError::new_spanned(
123            &ast.ident,
124            "Struct must have `name: String`.",
125        ));
126    }
127    let name_ty = &name_field.unwrap().ty;
128    let is_string = match name_ty {
129        syn::Type::Path(tp) => {
130            tp.path.segments.last().map(|seg| seg.ident == "String").unwrap_or(false)
131        }
132        _ => false,
133    };
134    if !is_string {
135        return Err(SynError::new_spanned(
136            name_ty,
137            "`name` field must be `String`",
138        ));
139    }
140
141    // require name_history if history=true
142    if cfg.history {
143        let hist_field = named_fields.iter().find(|field| {
144            field.ident.as_ref().map(|id| id == "name_history").unwrap_or(false)
145        });
146        if hist_field.is_none() {
147            return Err(SynError::new_spanned(
148                &ast.ident,
149                "history=true but no `name_history: Vec<String>` field found.",
150            ));
151        }
152    }
153
154    // require aliases if aliases=true
155    if cfg.aliases {
156        let alias_field = named_fields.iter().find(|field| {
157            field.ident.as_ref().map(|id| id == "aliases").unwrap_or(false)
158        });
159        if alias_field.is_none() {
160            return Err(SynError::new_spanned(
161                &ast.ident,
162                "aliases=true but no `aliases: Vec<String>` field found.",
163            ));
164        }
165    }
166
167    // Fallback name if none is provided
168    let fallback_name = cfg.default_name.clone().unwrap_or_else(|| struct_name.to_string());
169
170    // ### 3) Generate the "baseline" Named, DefaultName, ResetName
171    let baseline_impl = quote! {
172        impl #impl_generics Named for #struct_name #ty_generics #where_clause {
173            fn name(&self) -> std::borrow::Cow<'_, str> {
174                std::borrow::Cow::from(&self.name)
175            }
176        }
177
178        impl #impl_generics DefaultName for #struct_name #ty_generics #where_clause {
179            fn default_name() -> std::borrow::Cow<'static, str> {
180                std::borrow::Cow::from(#fallback_name)
181            }
182        }
183
184        impl #impl_generics ResetName for #struct_name #ty_generics #where_clause {}
185    };
186
187    // ### 4) If we have history=true, we push to `name_history` each time we rename
188    let setname_impl = if cfg.history {
189        quote! {
190            impl #impl_generics SetName for #struct_name #ty_generics #where_clause {
191                fn set_name(&mut self, name: &str) -> Result<(), NameError> {
192                    // push history first
193                    self.name_history.push(name.to_string());
194
195                    // forbid empty if not default
196                    if name.is_empty() && name != &*Self::default_name() {
197                        return Err(NameError::EmptyName);
198                    }
199                    self.name = name.to_owned();
200                    Ok(())
201                }
202            }
203
204            impl #impl_generics NameHistory for #struct_name #ty_generics #where_clause {
205                fn add_name_to_history(&mut self, name: &str) {
206                    self.name_history.push(name.to_string());
207                }
208
209                fn name_history(&self) -> Vec<std::borrow::Cow<'_, str>> {
210                    self.name_history
211                        .iter()
212                        .map(|s| std::borrow::Cow::from(&s[..]))
213                        .collect()
214                }
215            }
216        }
217    } else {
218        quote! {
219            impl #impl_generics SetName for #struct_name #ty_generics #where_clause {
220                fn set_name(&mut self, name: &str) -> Result<(), NameError> {
221                    // forbid empty if not default
222                    if name.is_empty() && name != &*Self::default_name() {
223                        return Err(NameError::EmptyName);
224                    }
225                    self.name = name.to_owned();
226                    Ok(())
227                }
228            }
229        }
230    };
231
232    // ### 5) If aliases=true, implement NamedAlias
233    let alias_impl = if cfg.aliases {
234        let arr_tokens = cfg.default_aliases.iter().map(|s| quote! { #s.to_owned() });
235        quote! {
236            impl #impl_generics NamedAlias for #struct_name #ty_generics #where_clause {
237                fn add_alias(&mut self, alias: &str) {
238                    self.aliases.push(alias.to_string());
239                }
240                fn aliases(&self) -> Vec<std::borrow::Cow<'_, str>> {
241                    self.aliases
242                        .iter()
243                        .map(|s| std::borrow::Cow::from(&s[..]))
244                        .collect()
245                }
246                fn clear_aliases(&mut self) {
247                    self.aliases.clear();
248                }
249            }
250
251            impl #impl_generics #struct_name #ty_generics #where_clause {
252                pub fn default_aliases() -> Vec<std::borrow::Cow<'static, str>> {
253                    vec![
254                        #(std::borrow::Cow::from(#arr_tokens)),*
255                    ]
256                }
257            }
258        }
259    } else {
260        quote!()
261    };
262
263    // ### 6) Combine expansions
264    let expanded = quote! {
265        #baseline_impl
266        #setname_impl
267        #alias_impl
268    };
269
270    Ok(expanded.into())
271}