extern crate proc_macro;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::{format_ident, quote};
use syn::{Attribute, DeriveInput, Fields, Type, parse_macro_input, spanned::Spanned};
#[proc_macro_derive(Omittable, attributes(omit))]
pub fn derive_omittable(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let fields = match &input.data {
syn::Data::Struct(s) => match &s.fields {
Fields::Named(named) => &named.named,
_ => {
let err = syn::Error::new(
input.ident.span(),
"omitty: Omittable can only be derived for structs with named fields (not tuple structs or unit structs)",
);
return err.to_compile_error().into();
}
},
_ => {
let err = syn::Error::new(
input.ident.span(),
"omitty: Omittable can only be derived for structs (not enums or unions)",
);
return err.to_compile_error().into();
}
};
for field in fields {
if let Some(omit_attr) = field.attrs.iter().find(|a| path_is(a, "omit")) {
let (omit_list, _explicit_ident) = match parse_omit_attr(omit_attr) {
Ok(result) => result,
Err(err) => return err.to_compile_error().into(),
};
let base_ident = match &field.ty {
Type::Path(tp) if tp.qself.is_none() => tp
.path
.segments
.last()
.map(|seg| seg.ident.clone())
.unwrap_or_else(|| Ident::new("Unknown", Span::call_site())),
_ => {
let err = syn::Error::new(
field.ty.span(),
"omitty: field type must be a path type (e.g., User, MyType). Complex types like references, arrays, or tuples are not supported",
);
return err.to_compile_error().into();
}
};
let err = syn::Error::new(
field.span(),
format!(
"omitty: Cannot automatically generate omitted type for this field.\n\
Solution: Add #[derive(OmittableType)] to the base type '{}' or manually define the omitted type variant.",
base_ident
),
);
return err.to_compile_error().into();
}
}
TokenStream::new()
}
#[proc_macro_derive(OmittableType, attributes(omit_variant))]
pub fn derive_omittable_type(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let fields = match &input.data {
syn::Data::Struct(s) => match &s.fields {
Fields::Named(named) => &named.named,
_ => return TokenStream::new(),
},
_ => return TokenStream::new(),
};
let mut variants = Vec::<proc_macro2::TokenStream>::new();
for attr in &input.attrs {
if path_is(attr, "omit_variant") {
match parse_variant_attr(attr) {
Ok((variant_name, omit_list)) => {
let kept_fields = fields
.iter()
.filter(|f| {
let fname = f.ident.as_ref().unwrap().to_string();
!omit_list.iter().any(|x| x == &fname)
})
.collect::<Vec<_>>();
let vis = &input.vis;
let variant_ident = format_ident!("{}", variant_name);
variants.push(quote! {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
#vis struct #variant_ident {
#(#kept_fields),*
}
});
}
Err(err) => return err.to_compile_error().into(),
}
}
}
quote! {
#(#variants)*
}
.into()
}
fn path_is(a: &Attribute, name: &str) -> bool {
a.path().is_ident(name)
}
fn parse_omit_attr(attr: &Attribute) -> syn::Result<(Vec<String>, Option<String>)> {
let mut fields = Vec::<String>::new();
let mut explicit_ident: Option<String> = None;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("ident") {
let value = meta.value()?;
let s: syn::LitStr = value.parse()?;
explicit_ident = Some(s.value());
return Ok(());
}
if meta.path.is_ident("fields") {
meta.parse_nested_meta(|nested| {
let field_name = meta
.path
.get_ident()
.ok_or_else(|| nested.error("expected field name identifier"))?
.to_string();
fields.push(field_name);
Ok(())
})?;
return Ok(());
}
let field_name = meta
.path
.get_ident()
.ok_or_else(|| meta.error("expected field name identifier"))?
.to_string();
fields.push(field_name);
Ok(())
})?;
Ok((fields, explicit_ident))
}
fn parse_variant_attr(attr: &Attribute) -> syn::Result<(String, Vec<String>)> {
let mut name: Option<String> = None;
let mut fields = Vec::<String>::new();
attr.parse_nested_meta(|meta| {
if name.is_none() {
let variant_name = meta
.path
.get_ident()
.ok_or_else(|| meta.error("expected variant name as first argument"))?
.to_string();
name = Some(variant_name);
return Ok(());
}
let field_name = meta
.path
.get_ident()
.ok_or_else(|| meta.error("expected field name identifier"))?
.to_string();
fields.push(field_name);
Ok(())
})?;
let name = name.ok_or_else(|| {
syn::Error::new_spanned(
attr,
"missing variant name as first argument in omit_variant attribute. Usage: #[omit_variant(VariantName, field1, field2, ...)]",
)
})?;
Ok((name, fields))
}