use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, quote_spanned, ToTokens};
use syn::parse_macro_input;
use syn::spanned::Spanned;
use syn::{
Data, DataStruct, DeriveInput, Error, Fields, Item, Meta, PathArguments, Type, Visibility,
};
#[proc_macro]
pub fn parse_column_name(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let is_valid = |c: char| c.is_ascii_alphanumeric() || c == '_' || c == '.';
let err = match syn::parse(input) {
Ok(syn::Lit::Str(name)) => match name.value().chars().find(|c| !is_valid(*c)) {
Some(bad_char) => Error::new(name.span(), format!("Invalid character: {bad_char:?}")),
_ => {
let path = name.value();
let path = path.split('.').map(proc_macro2::Literal::string);
return quote_spanned! { name.span() => [#(#path),*] }.into();
}
},
Ok(lit) => Error::new(lit.span(), "Expected a string literal"),
Err(err) => err,
};
err.into_compile_error().into()
}
#[proc_macro_derive(ToSchema, attributes(allow_null_container_values))]
pub fn derive_to_schema(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_ident = input.ident;
let schema_fields = gen_schema_fields(&input.data);
let output = quote! {
#[automatically_derived]
impl delta_kernel::schema::ToSchema for #struct_ident {
fn to_schema() -> delta_kernel::schema::StructType {
use delta_kernel::schema::derive_macro_utils::{
ToDataType as _, GetStructField as _, GetNullableContainerStructField as _,
};
delta_kernel::schema::StructType::new_unchecked([
#schema_fields
])
}
}
};
proc_macro::TokenStream::from(output)
}
fn get_schema_name(name: &Ident) -> Ident {
let snake_name = name.to_string();
let mut next_caps = false;
let ret: String = snake_name
.chars()
.filter_map(|c| {
if c == '_' {
next_caps = true;
None
} else if next_caps {
next_caps = false;
Some(c.to_ascii_uppercase())
} else {
Some(c)
}
})
.collect();
Ident::new(&ret, name.span())
}
fn is_option_of_hashmap(seg: &syn::PathSegment) -> bool {
if seg.ident != "Option" {
return false;
}
let PathArguments::AngleBracketed(angle_args) = &seg.arguments else {
return false;
};
let Some(syn::GenericArgument::Type(Type::Path(inner_type))) = angle_args.args.first() else {
return false;
};
inner_type
.path
.segments
.last()
.is_some_and(|seg| seg.ident == "HashMap")
}
fn gen_schema_fields(data: &Data) -> TokenStream {
let fields = match data {
Data::Struct(DataStruct {
fields: Fields::Named(fields),
..
}) => &fields.named,
_ => {
return Error::new(
Span::call_site(),
"this derive macro only works on structs with named fields",
)
.to_compile_error()
}
};
let schema_fields = fields.iter().map(|field| {
let name = field.ident.as_ref().unwrap(); let name = get_schema_name(name);
let have_schema_null = field.attrs.iter().any(|attr| {
match &attr.meta {
Meta::Path(path) => path.get_ident().is_some_and(|ident| ident == "allow_null_container_values"),
_ => false,
}
});
match field.ty {
Type::Path(ref type_path) => {
let type_path_quoted = type_path.path.segments.iter().map(|segment| {
let segment_ident = &segment.ident;
match &segment.arguments {
PathArguments::None => quote! { #segment_ident :: },
PathArguments::AngleBracketed(angle_args) => quote! { #segment_ident::#angle_args :: },
_ => Error::new(segment.arguments.span(), "Can only handle <> type path args").to_compile_error()
}
});
if have_schema_null {
if let Some(last_seg) = type_path.path.segments.last() {
let is_valid =
last_seg.ident == "HashMap" || is_option_of_hashmap(last_seg);
if !is_valid {
return Error::new(
last_seg.ident.span(),
format!("Can only use allow_null_container_values on HashMap or Option<HashMap> fields, not {}", last_seg.ident)
).to_compile_error()
}
}
quote_spanned! { field.span() => #(#type_path_quoted)* get_nullable_container_struct_field(stringify!(#name))}
} else {
quote_spanned! { field.span() => #(#type_path_quoted)* get_struct_field(stringify!(#name))}
}
}
_ => Error::new(field.span(), format!("Can't handle type: {:?}", field.ty)).to_compile_error()
}
});
quote! { #(#schema_fields),* }
}
#[proc_macro_derive(IntoEngineData)]
pub fn into_engine_data_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_name = &input.ident;
let Data::Struct(DataStruct {
fields: Fields::Named(fields),
..
}) = &input.data
else {
return Error::new(
struct_name.span(),
"IntoEngineData can only be derived for structs with named fields",
)
.to_compile_error()
.into();
};
let fields = &fields.named;
let field_idents = fields.iter().map(|f| &f.ident);
let field_types: Vec<_> = fields.iter().map(|f| &f.ty).collect();
let expanded = quote! {
#[automatically_derived]
impl delta_kernel::IntoEngineData for #struct_name
where
#(#field_types: TryInto<delta_kernel::expressions::Scalar>,)*
#(delta_kernel::Error: From<<#field_types as TryInto<delta_kernel::expressions::Scalar>>::Error>,)*
{
fn into_engine_data(
self,
schema: delta_kernel::schema::SchemaRef,
engine: &dyn delta_kernel::Engine)
-> delta_kernel::DeltaResult<Box<dyn delta_kernel::EngineData>> {
use delta_kernel::EvaluationHandlerExtension as _;
let values = [
#(self.#field_idents.try_into()?),*
];
let evaluator = engine.evaluation_handler();
evaluator.create_one(schema, &values)
}
}
};
proc_macro::TokenStream::from(expanded)
}
#[proc_macro_attribute]
pub fn internal_api(
_attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let input = parse_macro_input!(item as Item);
let public_version = make_public(input.clone());
let output = quote! {
#[cfg(feature = "internal-api")]
#public_version
#[cfg(not(feature = "internal-api"))]
#input
};
output.into()
}
fn make_public(mut item: Item) -> Item {
fn set_pub(vis: &mut Visibility, span: Span) -> Result<(), syn::Error> {
if matches!(vis, Visibility::Public(_)) {
return Err(Error::new(
vis.span(),
"ineligible for #[internal_api]: item is already public",
));
}
*vis = Visibility::Public(syn::token::Pub { span });
Ok(())
}
macro_rules! set_vis {
($item:ident) => {{
let vis_span = $item.vis.span();
set_pub(&mut $item.vis, vis_span)
}};
}
let result = match &mut item {
Item::Fn(f) => set_vis!(f),
Item::Struct(s) => set_vis!(s),
Item::Enum(e) => set_vis!(e),
Item::Trait(t) => set_vis!(t),
Item::Type(t) => set_vis!(t),
Item::Use(u) => set_vis!(u),
Item::Static(s) => set_vis!(s),
Item::Const(c) => set_vis!(c),
Item::Union(u) => set_vis!(u),
_ => Err(Error::new(
item.span(),
format!("unsupported item type for #[internal_api]: {item:?}"),
)),
};
if let Err(err) = result {
let error = err.to_compile_error();
let mut tokens = item.to_token_stream();
tokens.extend(error);
return syn::parse_quote!(#tokens);
}
item
}