1#![cfg(not(tarpaulin_include))]
2#![warn(
3 clippy::pedantic,
4 clippy::nursery,
5 clippy::suspicious,
6 clippy::str_to_string,
7 clippy::string_to_string,
8 clippy::panic_in_result_fn,
9 missing_copy_implementations
10)]
11#![deny(clippy::all)]
12#![allow(clippy::module_name_repetitions, clippy::no_effect_underscore_binding)]
13const KEY_IDENT: &str = "key";
16const ID_IDENT: &str = "id";
17
18use proc_macro2::TokenStream;
19use quote::{quote, quote_spanned};
20use syn::{parse_macro_input, spanned::Spanned, Data, DeriveInput, Error, Field, Fields, Result};
21
22#[proc_macro_derive(IndexEntry, attributes(key))]
23pub fn derive_entity(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
24 let input = parse_macro_input!(input as DeriveInput);
25 parse(&input)
26 .unwrap_or_else(|err| err.to_compile_error())
27 .into()
28}
29
30fn parse(input: &DeriveInput) -> Result<TokenStream> {
31 let ident = input.ident.clone();
32
33 let data = match &input.data {
34 Data::Struct(st) => st,
35 _ => {
36 return Err(Error::new_spanned(
37 &input,
38 "IndexEntry can only be derived on structs",
39 ))
40 }
41 };
42
43 let named_fields = match data.fields {
44 Fields::Named(ref named) => &named.named,
45 _ => {
46 return Err(Error::new_spanned(
47 &data.fields,
48 "IndexEntry can only be derived on a struct with named fields",
49 ))
50 }
51 };
52
53 let fields = named_fields.into_iter().cloned().collect::<Vec<_>>();
54
55 let id_field = get_id_field(&fields).ok_or_else(|| {
56 Error::new_spanned(
57 &input,
58 "Expected a #[key] attribute or a field named `key` or `id`.",
59 )
60 })?;
61
62 let id_ident = id_field
63 .ident
64 .as_ref()
65 .ok_or_else(|| Error::new_spanned(id_field, "expected a named field"))?;
66
67 let id_type = id_field.ty.clone();
68
69 let id_span = id_field.span();
70
71 let implementation = quote_spanned! {id_span=>
72 #[automatically_derived]
73 impl ::starchart::IndexEntry for #ident {
74 type Key = #id_type;
75
76 fn key(&self) -> &Self::Key {
77 &self.#id_ident
78 }
79 }
80 };
81
82 let quote_impl = quote! {
83 #implementation
84 };
85
86 Ok(quote_impl)
87}
88
89fn get_id_field(fields: &[Field]) -> Option<&Field> {
90 for field in fields {
91 if field.attrs.iter().any(|attr| attr.path.is_ident(KEY_IDENT)) {
92 return Some(field);
93 }
94 }
95
96 for field in fields {
97 if field
98 .ident
99 .as_ref()
100 .map_or(false, |ident| ident == KEY_IDENT || ident == ID_IDENT)
101 {
102 return Some(field);
103 }
104 }
105
106 None
107}