use proc_macro::TokenStream;use proc_macro2::Span;use quote::quote;use syn::Attribute;use syn::Data;use syn::DataEnum;use syn::DataStruct;use syn::DeriveInput;use syn::Field;use syn::Fields;use syn::Ident;use syn::LitStr;use syn::Type;use syn::parse_macro_input;#[proc_macro_derive(ExperimentalApi, attributes(experimental))]pub fn derive_experimental_api(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); match &input.data { Data::Struct(data) => derive_for_struct(&input, data), Data::Enum(data) => derive_for_enum(&input, data), Data::Union(_) => { syn::Error::new_spanned(&input.ident, "ExperimentalApi does not support unions") .to_compile_error() .into() } }}fn derive_for_struct(input: &DeriveInput, data: &DataStruct) -> TokenStream { let name = &input.ident; let type_name_lit = LitStr::new(&name.to_string(), Span::call_site()); let (checks, experimental_fields, registrations) = match &data.fields { Fields::Named(named) => { let mut checks = Vec::new(); let mut experimental_fields = Vec::new(); let mut registrations = Vec::new(); for field in &named.named { if let Some(reason) = experimental_reason(&field.attrs) { let expr = experimental_presence_expr(field, /*tuple_struct*/ false); checks.push(quote! { if #expr { return Some(#reason); } }); if let Some(field_name) = field_serialized_name(field) { let field_name_lit = LitStr::new(&field_name, Span::call_site()); experimental_fields.push(quote! { crate::experimental_api::ExperimentalField { type_name: #type_name_lit, field_name: #field_name_lit, reason: #reason, } }); registrations.push(quote! { ::inventory::submit! { crate::experimental_api::ExperimentalField { type_name: #type_name_lit, field_name: #field_name_lit, reason: #reason, } } }); } } else if has_nested_experimental(field) { let Some(ident) = field.ident.as_ref() else { continue; }; checks.push(quote! { if let Some(reason) = crate::experimental_api::ExperimentalApi::experimental_reason(&self.#ident) { return Some(reason); } }); } } (checks, experimental_fields, registrations) } Fields::Unnamed(unnamed) => { let mut checks = Vec::new(); let mut experimental_fields = Vec::new(); let mut registrations = Vec::new(); for (index, field) in unnamed.unnamed.iter().enumerate() { if let Some(reason) = experimental_reason(&field.attrs) { let expr = index_presence_expr(index, &field.ty); checks.push(quote! { if #expr { return Some(#reason); } }); let field_name_lit = LitStr::new(&index.to_string(), Span::call_site()); experimental_fields.push(quote! { crate::experimental_api::ExperimentalField { type_name: #type_name_lit, field_name: #field_name_lit, reason: #reason, } }); registrations.push(quote! { ::inventory::submit! { crate::experimental_api::ExperimentalField { type_name: #type_name_lit, field_name: #field_name_lit, reason: #reason, } } }); } else if has_nested_experimental(field) { let index = syn::Index::from(index); checks.push(quote! { if let Some(reason) = crate::experimental_api::ExperimentalApi::experimental_reason(&self.#index) { return Some(reason); } }); } } (checks, experimental_fields, registrations) } Fields::Unit => (Vec::new(), Vec::new(), Vec::new()), }; let checks = if checks.is_empty() { quote! { None } } else { quote! { #(#checks)* None } }; let experimental_fields = if experimental_fields.is_empty() { quote! { &[] } } else { quote! { &[ #(#experimental_fields,)* ] } }; let expanded = quote! { #(#registrations)* impl #name { pub(crate) const EXPERIMENTAL_FIELDS: &'static [crate::experimental_api::ExperimentalField] = #experimental_fields; } impl crate::experimental_api::ExperimentalApi for #name { fn experimental_reason(&self) -> Option<&'static str> { #checks } } }; expanded.into()}fn derive_for_enum(input: &DeriveInput, data: &DataEnum) -> TokenStream { let name = &input.ident; let mut match_arms = Vec::new(); for variant in &data.variants { let variant_name = &variant.ident; let pattern = match &variant.fields { Fields::Named(_) => quote!(Self::#variant_name { .. }), Fields::Unnamed(_) => quote!(Self::#variant_name ( .. )), Fields::Unit => quote!(Self::#variant_name), }; let reason = experimental_reason(&variant.attrs); if let Some(reason) = reason { match_arms.push(quote! { #pattern => Some(#reason), }); } else { match_arms.push(quote! { #pattern => None, }); } } let expanded = quote! { impl crate::experimental_api::ExperimentalApi for #name { fn experimental_reason(&self) -> Option<&'static str> { match self { #(#match_arms)* } } } }; expanded.into()}fn experimental_reason(attrs: &[Attribute]) -> Option<LitStr> { attrs.iter().find_map(experimental_reason_attr)}fn experimental_reason_attr(attr: &Attribute) -> Option<LitStr> { if !attr.path().is_ident("experimental") { return None; } attr.parse_args::<LitStr>().ok()}fn has_nested_experimental(field: &Field) -> bool { field.attrs.iter().any(experimental_nested_attr)}fn experimental_nested_attr(attr: &Attribute) -> bool { if !attr.path().is_ident("experimental") { return false; } attr.parse_args::<Ident>() .is_ok_and(|ident| ident == "nested")}fn field_serialized_name(field: &Field) -> Option<String> { let ident = field.ident.as_ref()?; let name = ident.to_string(); Some(snake_to_camel(&name))}fn snake_to_camel(s: &str) -> String { let mut out = String::with_capacity(s.len()); let mut upper = false; for ch in s.chars() { if ch == '_' { upper = true; continue; } if upper { out.push(ch.to_ascii_uppercase()); upper = false; } else { out.push(ch); } } out}fn experimental_presence_expr( field: &Field, tuple_struct: bool,) -> Option<proc_macro2::TokenStream> { if tuple_struct { return None; } let ident = field.ident.as_ref()?; Some(presence_expr_for_access(quote!(self.#ident), &field.ty))}fn index_presence_expr(index: usize, ty: &Type) -> proc_macro2::TokenStream { let index = syn::Index::from(index); presence_expr_for_access(quote!(self.#index), ty)}fn presence_expr_for_access( access: proc_macro2::TokenStream, ty: &Type,) -> proc_macro2::TokenStream { if option_inner(ty).is_some() { return quote! { #access.is_some() }; } if is_vec_like(ty) || is_map_like(ty) { return quote! { !#access.is_empty() }; } if is_bool(ty) { return quote! { #access }; } quote! { true }}fn option_inner(ty: &Type) -> Option<&Type> { let Type::Path(type_path) = ty else { return None; }; let segment = type_path.path.segments.last()?; if segment.ident != "Option" { return None; } let syn::PathArguments::AngleBracketed(args) = &segment.arguments else { return None; }; args.args.iter().find_map(|arg| match arg { syn::GenericArgument::Type(inner) => Some(inner), _ => None, })}fn is_vec_like(ty: &Type) -> bool { type_last_ident(ty).is_some_and(|ident| ident == "Vec")}fn is_map_like(ty: &Type) -> bool { type_last_ident(ty).is_some_and(|ident| ident == "HashMap" || ident == "BTreeMap")}fn is_bool(ty: &Type) -> bool { type_last_ident(ty).is_some_and(|ident| ident == "bool")}fn type_last_ident(ty: &Type) -> Option<Ident> { let Type::Path(type_path) = ty else { return None; }; type_path.path.segments.last().map(|seg| seg.ident.clone())}