use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{Data, DeriveInput, Fields, GenericArgument, PathArguments, Type, TypePath};
pub(crate) fn derive(input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as DeriveInput);
let struct_name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let named_fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => &fields.named,
_ => {
return syn::Error::new_spanned(
struct_name,
"`#[derive(Inspect)]` requires a struct with named fields",
)
.to_compile_error()
.into();
}
},
_ => {
return syn::Error::new_spanned(
struct_name,
"`#[derive(Inspect)]` only supports structs",
)
.to_compile_error()
.into();
}
};
let mut accessors = Vec::new();
for field in named_fields {
let args = match parse_inspect_attr(field) {
Ok(Some(a)) => a,
Ok(None) => continue,
Err(err) => return err.to_compile_error().into(),
};
let field_name = field.ident.as_ref().expect("named field has an ident");
let (key_ty, value_ty) = match extract_skipmap_kv(&field.ty) {
Some(kv) => kv,
None => {
return syn::Error::new_spanned(
field_name,
"`#[inspect(...)]` only supports `SkipMap<K, V>` fields in v1 \
(other collection types and scalars are deferred to a future release)",
)
.to_compile_error()
.into();
}
};
let method_suffix = args.name.unwrap_or_else(|| field_name.to_string());
let method_ident = format_ident!("inspect_{}", method_suffix);
let (return_elem_ty, projection_expr) = match &args.snapshot_ty {
Some(t) => (
quote!(#t),
quote!(<#t as ::core::convert::From<&#value_ty>>::from(e.value())),
),
None => (
quote!(#value_ty),
quote!(::core::clone::Clone::clone(e.value())),
),
};
accessors.push(quote! {
pub fn #method_ident(&self) -> ::std::vec::Vec<(#key_ty, #return_elem_ty)> {
self.#field_name
.iter()
.map(|e| (*e.key(), #projection_expr))
.collect()
}
});
}
let expanded = quote! {
impl #impl_generics #struct_name #ty_generics #where_clause {
#(#accessors)*
}
};
expanded.into()
}
struct InspectArgs {
snapshot_ty: Option<Type>,
name: Option<String>,
}
fn parse_inspect_attr(field: &syn::Field) -> syn::Result<Option<InspectArgs>> {
let mut found: Option<&syn::Attribute> = None;
for attr in &field.attrs {
if attr.path().is_ident("inspect") {
if found.is_some() {
return Err(syn::Error::new_spanned(
attr,
"multiple `#[inspect(...)]` attributes on one field; combine them",
));
}
found = Some(attr);
}
}
let Some(attr) = found else { return Ok(None) };
let mut snapshot_ty: Option<Type> = None;
let mut name: Option<String> = None;
if matches!(attr.meta, syn::Meta::Path(_)) {
return Ok(Some(InspectArgs { snapshot_ty, name }));
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("name") {
let value = meta.value()?;
let lit: syn::LitStr = value.parse()?;
name = Some(lit.value());
Ok(())
} else if meta.input.peek(syn::Token![=]) {
Err(meta.error(
"unknown `inspect` argument; expected positional `T` (projection type) or `name = \"...\"`",
))
} else if snapshot_ty.is_none() {
let path = meta.path.clone();
snapshot_ty = Some(syn::parse_quote!(#path));
Ok(())
} else {
Err(meta.error(
"`#[inspect(...)]` accepts at most one positional projection type",
))
}
})?;
Ok(Some(InspectArgs { snapshot_ty, name }))
}
fn extract_skipmap_kv(ty: &Type) -> Option<(Type, Type)> {
let Type::Path(TypePath { path, .. }) = ty else {
return None;
};
let last = path.segments.last()?;
if last.ident != "SkipMap" {
return None;
}
let PathArguments::AngleBracketed(args) = &last.arguments else {
return None;
};
let mut iter = args.args.iter();
let k = match iter.next()? {
GenericArgument::Type(t) => t.clone(),
_ => return None,
};
let v = match iter.next()? {
GenericArgument::Type(t) => t.clone(),
_ => return None,
};
Some((k, v))
}