starchart_derive/
lib.rs

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)]
13//! Derive macro helpers for the starchart crate.
14
15const 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}