use heck::{ToKebabCase, ToLowerCamelCase, ToPascalCase, ToShoutySnakeCase, ToSnakeCase};
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::quote;
use syn::spanned::Spanned;
use syn::{
Attribute, Data, DataStruct, DeriveInput, Field, Fields, FieldsNamed, LitStr, Path,
WhereClause, parse_macro_input, parse_quote,
};
#[proc_macro_derive(MaskSpec, attributes(serde, fieldmasker))]
pub fn derive_mask_spec(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let ident = &input.ident;
let krate = syn::parse_quote!(::fieldmasker);
let (impl_generics, ty_generics, mut where_clause) = {
let (ig, tg, wc) = input.generics.split_for_impl();
(ig, tg, wc.cloned())
};
let rename_all = container_rename_all(&input.attrs);
let expanded: TokenStream2 = match &input.data {
Data::Struct(DataStruct {
fields: Fields::Named(fields),
..
}) => {
if let Err(e) = check_named_fields_supported(fields) {
return e.to_compile_error().into();
}
where_clause = add_bounds_for_named_fields(where_clause, fields);
let entries: Vec<TokenStream2> = fields
.named
.iter()
.filter(|f| !should_skip(f))
.map(|f| field_entry(f, rename_all.as_deref(), &krate))
.collect();
let build_node = quote! { #krate::spec::Node::object([#(#entries),*]) };
quote! {
impl #impl_generics #krate::MaskSpec for #ident #ty_generics #where_clause {
fn mask_spec() -> &'static #krate::spec::Node {
static __NODE: ::std::sync::OnceLock<#krate::spec::Node> = ::std::sync::OnceLock::new();
__NODE.get_or_init(|| { #build_node })
}
}
}
}
Data::Struct(DataStruct {
fields: Fields::Unit,
..
}) => {
quote! {
impl #impl_generics #krate::MaskSpec for #ident #ty_generics #where_clause {
fn mask_spec() -> &'static #krate::spec::Node {
static __NODE: #krate::spec::Node = #krate::spec::Node::scalar();
&__NODE
}
}
}
}
Data::Struct(DataStruct {
fields: Fields::Unnamed(tup),
..
}) => {
if tup.unnamed.len() == 1 {
let inner = &tup.unnamed[0].ty;
quote! {
impl #impl_generics #krate::MaskSpec for #ident #ty_generics #where_clause {
fn mask_spec() -> &'static #krate::spec::Node {
<#inner as #krate::MaskSpec>::mask_spec()
}
}
}
} else {
quote! {
impl #impl_generics #krate::MaskSpec for #ident #ty_generics #where_clause {
fn mask_spec() -> &'static #krate::spec::Node {
static __NODE: #krate::spec::Node = #krate::spec::Node::scalar();
&__NODE
}
}
}
}
}
Data::Enum(_) | Data::Union(_) => {
quote! {
impl #impl_generics #krate::MaskSpec for #ident #ty_generics #where_clause {
fn mask_spec() -> &'static #krate::spec::Node {
static __NODE: #krate::spec::Node = #krate::spec::Node::scalar();
&__NODE
}
}
}
}
};
expanded.into()
}
fn check_named_fields_supported(fields: &FieldsNamed) -> syn::Result<()> {
for f in &fields.named {
if has_flatten(f) {
return Err(syn::Error::new(
f.span(),
"`#[serde(flatten)]` is not supported by `fieldmasker::MaskSpec` derive yet",
));
}
}
Ok(())
}
fn add_bounds_for_named_fields(
where_clause: Option<WhereClause>,
fields: &FieldsNamed,
) -> Option<WhereClause> {
let mut wc = where_clause.unwrap_or_else(|| WhereClause {
where_token: Default::default(),
predicates: Default::default(),
});
for f in &fields.named {
if should_skip(f) {
continue;
}
let ty = &f.ty;
wc.predicates
.push(parse_quote!(#ty : fieldmasker::MaskSpec));
}
Some(wc)
}
fn field_entry(field: &Field, container_rename_all: Option<&str>, krate: &Path) -> TokenStream2 {
let name_lit = resolve_field_name(field, container_rename_all);
let ty = &field.ty;
quote! {
(#name_lit, <#ty as #krate::MaskSpec>::mask_spec().clone())
}
}
fn should_skip(field: &Field) -> bool {
field.attrs.iter().any(|attr| {
if !is_serde_attr(attr) {
return false;
}
let mut skip = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("skip") || meta.path.is_ident("skip_serializing") {
skip = true;
}
Ok(())
});
skip
})
}
fn has_flatten(field: &Field) -> bool {
field.attrs.iter().any(|attr| {
if !is_serde_attr(attr) {
return false;
}
let mut yes = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("flatten") {
yes = true;
}
Ok(())
});
yes
})
}
fn container_rename_all(attrs: &[Attribute]) -> Option<String> {
for attr in attrs {
if !is_serde_attr(attr) {
continue;
}
let mut out: Option<String> = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename_all") {
let lit: LitStr = meta.value()?.parse()?;
out = Some(lit.value());
}
Ok(())
});
if out.is_some() {
return out;
}
}
None
}
fn is_serde_attr(attr: &Attribute) -> bool {
attr.path().is_ident("serde")
}
fn resolve_field_name(field: &Field, container_rename_all: Option<&str>) -> LitStr {
for attr in &field.attrs {
if let Some(lit) = serde_serialize_name(attr, field) {
return lit;
}
}
let ident = field.ident.as_ref().expect("named fields only");
let base = ident.to_string();
let named = apply_rename_all(&base, container_rename_all);
LitStr::new(&named, ident.span())
}
fn serde_serialize_name(attr: &Attribute, field: &Field) -> Option<LitStr> {
if !is_serde_attr(attr) {
return None;
}
let mut out: Option<LitStr> = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
if meta.input.peek(syn::Token![=]) {
let lit: LitStr = meta.value()?.parse()?;
out = Some(with_span_from(&lit, field));
} else {
let mut ser: Option<LitStr> = None;
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("serialize") {
let lit: LitStr = inner.value()?.parse()?;
ser = Some(with_span_from(&lit, field));
}
Ok(())
})?;
if let Some(l) = ser {
out = Some(l);
}
}
}
Ok(())
});
out
}
fn apply_rename_all(base: &str, ra: Option<&str>) -> String {
match ra {
Some("lowercase") => base.to_lowercase(),
Some("UPPERCASE") => base.to_uppercase(),
Some("snake_case") => base.to_snake_case(),
Some("SCREAMING_SNAKE_CASE") => base.to_shouty_snake_case(),
Some("kebab-case") => base.to_kebab_case(),
Some("SCREAMING-KEBAB-CASE") => base.to_kebab_case().to_uppercase(),
Some("camelCase") => base.to_lower_camel_case(),
Some("PascalCase") => base.to_pascal_case(),
_ => base.to_owned(),
}
}
fn with_span_from(lit: &LitStr, field: &Field) -> LitStr {
let span = field
.ident
.as_ref()
.map(|id| id.span())
.unwrap_or_else(Span::call_site);
LitStr::new(&lit.value(), span)
}