use std::collections::hash_map::Entry as MapEntry;
use std::collections::HashMap;
use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned, ToTokens};
use syn::parse::{ParseStream, Parser};
use syn::spanned::Spanned;
use syn::token::{Brace, Bracket, Paren};
use syn::{braced, bracketed, parenthesized, Expr, Ident, LitStr, Token};
mod kw {
syn::custom_keyword!(nullable);
syn::custom_keyword!(not_null);
}
pub(crate) fn parse_schema(
input: proc_macro::TokenStream,
fallible: bool,
wrap: impl FnOnce(TokenStream) -> TokenStream,
) -> proc_macro::TokenStream {
let compile_errors_block = |errors: Vec<syn::Error>| {
let invocations = errors.iter().map(syn::Error::to_compile_error);
quote!({ #( #invocations )* })
};
try_parse_schema(input.into(), fallible)
.map_or_else(compile_errors_block, wrap)
.into()
}
fn try_parse_schema(input: TokenStream, fallible: bool) -> Result<TokenStream, Vec<syn::Error>> {
let mut errors: Vec<syn::Error> = Vec::new();
let parser = |input: ParseStream| emit_struct(input, fallible, &mut errors);
match parser.parse2(input) {
Ok(block) if errors.is_empty() => Ok(block),
Ok(_) => Err(errors),
Err(parse_error) => Err(vec![parse_error]),
}
}
fn emit_struct(
input: ParseStream,
fallible: bool,
errors: &mut Vec<syn::Error>,
) -> syn::Result<TokenStream> {
let mut literals = HashMap::new();
let mut idents = HashMap::new();
let mut stmts = Vec::new();
while !input.is_empty() {
let entry = emit_field_entry(input, fallible, &mut literals, &mut idents, errors);
stmts.push(entry?);
if input.is_empty() {
break;
}
input.parse::<Token![,]>()?;
}
let ctor = if fallible {
quote!(try_new)
} else {
quote!(new_unchecked)
};
Ok(quote! {{
let mut __fields = ::std::vec::Vec::new();
#( #stmts )*
delta_kernel::schema::StructType::#ctor(__fields)
}})
}
fn emit_field_entry(
input: ParseStream,
fallible: bool,
literals: &mut HashMap<String, Span>,
idents: &mut HashMap<String, Span>,
errors: &mut Vec<syn::Error>,
) -> syn::Result<TokenStream> {
if input.peek(Token![..]) {
input.parse::<Token![..]>()?;
let expr: Expr = parse_paren_to_end(input, "splice expression")?;
return Ok(quote_spanned! { expr.span() =>
__fields.extend(
::core::iter::IntoIterator::into_iter(#expr)
.map(delta_kernel::schema::ToSchemaField::to_schema_field)
);
});
}
if input.peek(Paren) {
let expr: Expr = parse_paren_to_end(input, "field expression")?;
return Ok(quote_spanned! { expr.span() =>
__fields.push(delta_kernel::schema::ToSchemaField::to_schema_field(#expr));
});
}
let nullable = parse_nullability(input)?;
let name = emit_field_name(input, literals, idents, errors)?;
input.parse::<Token![:]>()?;
let dtype = emit_type(input, fallible, errors)?;
Ok(quote! {
__fields.push(delta_kernel::schema::StructField::new(#name, #dtype, #nullable));
})
}
fn emit_field_name(
input: ParseStream,
literals: &mut HashMap<String, Span>,
idents: &mut HashMap<String, Span>,
errors: &mut Vec<syn::Error>,
) -> syn::Result<TokenStream> {
if input.peek(LitStr) {
let lit: LitStr = input.parse()?;
check_for_duplicates(literals, lit.value().to_lowercase(), lit.span(), errors);
Ok(lit.to_token_stream())
} else if input.peek(Paren) {
let expr: Expr = parse_paren_to_end(input, "field name expression")?;
Ok(expr.to_token_stream())
} else {
let ident: Ident = input.parse()?;
check_for_duplicates(idents, ident.to_string(), ident.span(), errors);
Ok(ident.to_token_stream())
}
}
fn emit_type(
input: ParseStream,
fallible: bool,
errors: &mut Vec<syn::Error>,
) -> syn::Result<TokenStream> {
if input.peek(Paren) {
let expr: Expr = parse_paren_to_end(input, "parenthesized type")?;
return Ok(expr.to_token_stream());
}
if input.peek(Bracket) {
let content;
bracketed!(content in input);
let contains_null = parse_nullability(&content)?;
let element = emit_type(&content, fallible, errors)?;
ensure_empty(&content, "array element type")?;
return Ok(quote! { delta_kernel::schema::ArrayType::new(#element, #contains_null) });
}
if input.peek(Brace) {
let content;
braced!(content in input);
if !content.peek2(Token![=>]) {
let block = emit_struct(&content, fallible, errors)?;
return Ok(if fallible { quote!(#block?) } else { block });
}
let key = emit_type(&content, fallible, errors)?;
content.parse::<Token![=>]>()?;
let value_contains_null = parse_nullability(&content)?;
let value = emit_type(&content, fallible, errors)?;
ensure_empty(&content, "map value type")?;
return Ok(
quote! { delta_kernel::schema::MapType::new(#key, #value, #value_contains_null) },
);
}
let expr: Expr = input.parse()?;
validate_bare_datatype(&expr)?;
Ok(quote_spanned! { expr.span() => delta_kernel::schema::DataType::#expr })
}
fn parse_nullability(input: ParseStream) -> syn::Result<bool> {
if input.peek(kw::nullable) {
input.parse::<kw::nullable>()?;
Ok(true)
} else if input.peek(kw::not_null) {
input.parse::<kw::not_null>()?;
Ok(false)
} else {
Err(input.error("expected `nullable` or `not_null`"))
}
}
fn validate_bare_datatype(expr: &Expr) -> syn::Result<()> {
let path = match expr {
Expr::Path(path) if path.qself.is_none() => &path.path,
Expr::Call(call) => match &*call.func {
Expr::Path(path) if path.qself.is_none() => &path.path,
other => return Err(syn::Error::new_spanned(other, "expected a data type")),
},
other => return Err(syn::Error::new_spanned(other, "expected a data type")),
};
if path.leading_colon.is_some() || path.segments.len() != 1 {
return Err(syn::Error::new_spanned(path, "unexpected qualified path"));
}
Ok(())
}
fn parse_paren_to_end<T: syn::parse::Parse>(input: ParseStream, what: &str) -> syn::Result<T> {
let content;
parenthesized!(content in input);
let value = content.parse()?;
ensure_empty(&content, what)?;
Ok(value)
}
fn ensure_empty(content: ParseStream, what: &str) -> syn::Result<()> {
if content.is_empty() {
Ok(())
} else {
Err(content.error(format!("unexpected tokens after {what}")))
}
}
fn check_for_duplicates(
seen: &mut HashMap<String, Span>,
key: String,
span: Span,
errors: &mut Vec<syn::Error>,
) {
match seen.entry(key) {
MapEntry::Occupied(first) => {
errors.push(syn::Error::new(span, "duplicate field name"));
errors.push(syn::Error::new(*first.get(), "first defined here"));
}
MapEntry::Vacant(slot) => {
slot.insert(span);
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case::missing_top_level_nullability(quote! { "a": INTEGER }, "nullable")]
#[case::missing_array_element_nullability(quote! { nullable "a": [ STRING ] }, "nullable")]
#[case::missing_map_value_nullability(quote! { nullable "a": { STRING => STRING } }, "nullable")]
#[case::qualified_path_type(quote! { nullable "a": a::b }, "qualified path")]
#[case::non_datatype_bare_type(quote! { nullable "a": 42 }, "data type")]
#[case::trailing_tokens_in_parenthesized_type(quote! { nullable "a": (foo bar) }, "unexpected tokens")]
#[case::trailing_tokens_in_array_element(quote! { nullable "a": [ nullable STRING extra ] }, "unexpected tokens")]
#[case::trailing_tokens_in_map_value(quote! { nullable "a": { STRING => nullable STRING extra } }, "unexpected tokens")]
fn rejects_with_diagnostic(#[case] input: TokenStream, #[case] needle: &str) {
let errors = try_parse_schema(input, false).expect_err("expected rejection");
let messages = Vec::from_iter(errors.iter().map(ToString::to_string));
assert!(
messages.iter().any(|m| m.contains(needle)),
"no error contained {needle:?}; got: {messages:?}",
);
}
#[rstest]
#[case::missing_colon(quote! { nullable "a" INTEGER })]
#[case::missing_comma_between_fields(quote! { not_null "a": INTEGER not_null "b": LONG })]
fn rejects_structurally_invalid_input(#[case] input: TokenStream) {
assert!(try_parse_schema(input, false).is_err());
}
#[rstest]
#[case::case_insensitive_string_literals(quote! { nullable "id": INTEGER, nullable "ID": STRING })]
#[case::repeated_identifiers(quote! { nullable NAME: INTEGER, not_null NAME: STRING })]
fn rejects_duplicate_field_names(#[case] input: TokenStream) {
let errors = try_parse_schema(input, false).expect_err("expected duplicate rejection");
let messages = Vec::from_iter(errors.iter().map(ToString::to_string));
assert!(
messages.iter().any(|m| m.contains("duplicate field name")),
"got: {messages:?}"
);
assert!(
messages.iter().any(|m| m.contains("first defined here")),
"got: {messages:?}"
);
}
#[rstest]
#[case::flat_fields(quote! {
not_null "id": LONG,
nullable "name": STRING,
not_null "score": (DataType::DOUBLE),
})]
#[case::nested_struct_array_and_map(quote! {
not_null "user": { nullable "city": STRING, nullable "zip": STRING },
nullable "tags": [ not_null STRING ],
not_null "props": { STRING => nullable STRING },
})]
#[case::interpolated_name_type_field_and_splice(quote! {
not_null (format!("col_{}", 1)): (DataType::LONG),
(StructField::nullable("z", DataType::STRING)),
..(::std::vec::Vec::<StructField>::new()),
})]
#[case::empty_body(quote! {})]
#[case::same_name_in_sibling_nested_structs(quote! {
not_null "a": { nullable "x": INTEGER },
not_null "b": { nullable "x": INTEGER },
})]
#[case::interpolated_name_collision_deferred(quote! {
nullable (n): INTEGER,
nullable (n): STRING,
})]
fn accepts_well_formed_schema(
#[case] input: TokenStream,
#[values(false, true)] fallible: bool,
) {
let result = try_parse_schema(input, fallible);
assert!(
result.is_ok(),
"expected acceptance, got: {:?}",
result
.err()
.map(|e| Vec::from_iter(e.iter().map(ToString::to_string))),
);
}
}