1use proc_macro::TokenStream;
4use quote::quote;
5use syn::{
6 parse_macro_input, DeriveInput, Data, Fields, Error as SynError,
7 LitStr,
8};
9
10#[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 let config = match parse_named_item_attrs(&ast) {
18 Ok(cfg) => cfg,
19 Err(e) => return e.to_compile_error().into(),
20 };
21
22 match impl_named_item(&ast, &config) {
24 Ok(ts) => ts,
25 Err(err) => err.to_compile_error().into(),
26 }
27}
28
29struct NamedItemConfig {
31 default_name: Option<String>,
33 aliases: bool,
35 default_aliases: Vec<String>,
37 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 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
83fn impl_named_item(ast: &DeriveInput, cfg: &NamedItemConfig) -> syn::Result<TokenStream> {
91 let struct_name = &ast.ident;
92
93 let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
95
96 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 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 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 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 let fallback_name = cfg.default_name.clone().unwrap_or_else(|| struct_name.to_string());
169
170 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 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 self.name_history.push(name.to_string());
194
195 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 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 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 let expanded = quote! {
265 #baseline_impl
266 #setname_impl
267 #alias_impl
268 };
269
270 Ok(expanded.into())
271}