use std::collections::HashSet;
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use syn::{GenericArgument, PathArguments, Type};
use crate::parser::schema::{is_builtin_openapi_type, schema_attrs::SchemaConstraints};
#[must_use]
pub fn emit_schema_supplements(
input: &syn::DeriveInput,
field_constraints: &[SchemaConstraints],
) -> TokenStream {
let marker = emit_marker_impl(input);
let assertions = emit_field_assertions(input, field_constraints);
quote! {
#marker
#assertions
}
}
fn emit_marker_impl(input: &syn::DeriveInput) -> TokenStream {
let name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
quote! {
#[automatically_derived]
impl #impl_generics ::vespera::Schema for #name #ty_generics #where_clause {}
}
}
fn emit_field_assertions(
input: &syn::DeriveInput,
field_constraints: &[SchemaConstraints],
) -> TokenStream {
let syn::Data::Struct(data_struct) = &input.data else {
return TokenStream::new();
};
let syn::Fields::Named(fields_named) = &data_struct.fields else {
return TokenStream::new();
};
let generic_idents: HashSet<String> = input
.generics
.params
.iter()
.filter_map(|param| {
if let syn::GenericParam::Type(t) = param {
Some(t.ident.to_string())
} else {
None
}
})
.collect();
let mut blocks: Vec<TokenStream> = Vec::new();
for (field, constraints) in fields_named.named.iter().zip(field_constraints) {
if constraints.is_any() {
continue;
}
for (leaf, last_ident) in collect_leaf_custom_types(&field.ty) {
if type_contains_generic(&leaf, &generic_idents) {
continue;
}
if is_builtin_openapi_type(&last_ident) {
continue;
}
if is_serde_json_value_leaf(&leaf) {
continue;
}
let field_span = field.span();
blocks.push(quote_spanned! { field_span =>
const _: () = {
const fn __vespera_assert_schema<T: ::vespera::Schema>() {}
let _ = __vespera_assert_schema::<#leaf>;
};
});
}
}
quote! { #(#blocks)* }
}
fn collect_leaf_custom_types(ty: &Type) -> Vec<(Type, String)> {
let Type::Path(type_path) = ty else {
return Vec::new();
};
let Some(segment) = type_path.path.segments.last() else {
return Vec::new();
};
if segment.ident == "Vec"
|| segment.ident == "Option"
|| segment.ident == "Box"
|| segment.ident == "HashSet"
|| segment.ident == "BTreeSet"
{
return first_generic_type_arg(segment)
.map(collect_leaf_custom_types)
.unwrap_or_default();
}
if segment.ident == "HashMap" || segment.ident == "BTreeMap" {
return second_generic_type_arg(segment)
.map(collect_leaf_custom_types)
.unwrap_or_default();
}
vec![(ty.clone(), segment.ident.to_string())]
}
fn first_generic_type_arg(segment: &syn::PathSegment) -> Option<&Type> {
let PathArguments::AngleBracketed(args) = &segment.arguments else {
return None;
};
args.args.iter().find_map(|arg| {
if let GenericArgument::Type(inner) = arg {
Some(inner)
} else {
None
}
})
}
fn second_generic_type_arg(segment: &syn::PathSegment) -> Option<&Type> {
let PathArguments::AngleBracketed(args) = &segment.arguments else {
return None;
};
args.args
.iter()
.filter_map(|arg| {
if let GenericArgument::Type(inner) = arg {
Some(inner)
} else {
None
}
})
.nth(1)
}
fn type_contains_generic(ty: &Type, generic_idents: &HashSet<String>) -> bool {
match ty {
Type::Path(type_path) => {
for segment in &type_path.path.segments {
if generic_idents.contains(&segment.ident.to_string()) {
return true;
}
if let PathArguments::AngleBracketed(args) = &segment.arguments {
for arg in &args.args {
if let GenericArgument::Type(inner) = arg
&& type_contains_generic(inner, generic_idents)
{
return true;
}
}
}
}
false
}
Type::Reference(r) => type_contains_generic(&r.elem, generic_idents),
_ => false,
}
}
fn is_serde_json_value_leaf(ty: &Type) -> bool {
let Type::Path(type_path) = ty else {
return false;
};
let segments = &type_path.path.segments;
let Some(last) = segments.last() else {
return false;
};
if last.ident != "Value" {
return false;
}
if segments.len() == 1 {
return true;
}
segments.iter().any(|segment| segment.ident == "serde_json")
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
use syn::parse_quote;
fn parse_ty(src: &str) -> Type {
syn::parse_str::<Type>(src).expect("valid type")
}
fn leaves_of(src: &str) -> Vec<String> {
collect_leaf_custom_types(&parse_ty(src))
.iter()
.map(|(ty, _)| quote!(#ty).to_string().replace(' ', ""))
.collect()
}
fn empty_path_type() -> Type {
Type::Path(syn::TypePath {
attrs: Vec::new(),
qself: None,
path: syn::Path {
leading_colon: None,
segments: syn::punctuated::Punctuated::new(),
},
})
}
fn constraints_for(input: &syn::DeriveInput) -> Vec<SchemaConstraints> {
use crate::parser::schema::schema_attrs::try_extract_schema_constraints;
if let syn::Data::Struct(d) = &input.data
&& let syn::Fields::Named(f) = &d.fields
{
return f
.named
.iter()
.map(|fld| try_extract_schema_constraints(&fld.attrs).unwrap_or_default())
.collect();
}
Vec::new()
}
#[test]
fn leaf_of_plain_path_is_itself() {
assert_eq!(leaves_of("MyType"), vec!["MyType"]);
}
#[test]
fn leaf_of_vec_unwraps_inner() {
assert_eq!(leaves_of("Vec<MyType>"), vec!["MyType"]);
}
#[test]
fn leaf_of_option_unwraps_inner() {
assert_eq!(leaves_of("Option<MyType>"), vec!["MyType"]);
}
#[test]
fn leaf_of_box_unwraps_inner() {
assert_eq!(leaves_of("Box<MyType>"), vec!["MyType"]);
}
#[test]
fn leaf_of_hashset_unwraps_inner() {
assert_eq!(leaves_of("HashSet<MyType>"), vec!["MyType"]);
}
#[test]
fn leaf_of_btreeset_unwraps_inner() {
assert_eq!(leaves_of("BTreeSet<MyType>"), vec!["MyType"]);
}
#[test]
fn leaf_of_hashmap_unwraps_value_not_key() {
assert_eq!(leaves_of("HashMap<String, MyType>"), vec!["MyType"]);
}
#[test]
fn leaf_of_btreemap_unwraps_value_not_key() {
assert_eq!(leaves_of("BTreeMap<String, MyType>"), vec!["MyType"]);
}
#[test]
fn leaf_of_nested_wrappers() {
assert_eq!(
leaves_of("Vec<Option<Box<HashMap<String, MyType>>>>"),
vec!["MyType"]
);
}
#[test]
fn leaf_of_non_path_type_is_empty() {
assert!(collect_leaf_custom_types(&parse_ty("(i32, String)")).is_empty());
assert!(collect_leaf_custom_types(&parse_ty("&MyType")).is_empty());
}
#[test]
fn leaf_of_empty_wrapper_is_empty() {
assert!(collect_leaf_custom_types(&parse_ty("Vec")).is_empty());
}
#[test]
fn leaf_of_empty_path_is_empty() {
assert!(collect_leaf_custom_types(&empty_path_type()).is_empty());
}
#[test]
fn generic_argument_helpers_ignore_lifetimes_and_missing_arguments() {
let vec_lifetime = parse_ty("Vec<'a>");
let Type::Path(vec_path) = vec_lifetime else {
panic!("fixture must be a path type");
};
let vec_segment = vec_path.path.segments.last().expect("Vec segment");
let bare_map = parse_ty("HashMap");
let Type::Path(bare_map_path) = bare_map else {
panic!("fixture must be a path type");
};
let bare_map_segment = bare_map_path.path.segments.last().expect("HashMap segment");
let lifetime_map = parse_ty("HashMap<'a, 'b>");
let Type::Path(lifetime_map_path) = lifetime_map else {
panic!("fixture must be a path type");
};
let lifetime_map_segment = lifetime_map_path
.path
.segments
.last()
.expect("HashMap segment");
assert_eq!(first_generic_type_arg(vec_segment), None);
assert_eq!(second_generic_type_arg(bare_map_segment), None);
assert_eq!(second_generic_type_arg(lifetime_map_segment), None);
}
#[test]
fn leaf_pairs_carry_the_final_path_ident() {
let leaves = collect_leaf_custom_types(&parse_ty("Vec<Option<crate::models::User>>"));
assert_eq!(leaves.len(), 1);
assert_eq!(leaves[0].1, "User");
}
#[test]
fn bare_value_is_allowlisted() {
assert!(is_serde_json_value_leaf(&parse_ty("Value")));
}
#[test]
fn serde_json_value_is_allowlisted() {
assert!(is_serde_json_value_leaf(&parse_ty("serde_json::Value")));
}
#[test]
fn leading_colon_serde_json_value_is_allowlisted() {
assert!(is_serde_json_value_leaf(&parse_ty("::serde_json::Value")));
}
#[test]
fn vespera_reexport_serde_json_value_is_allowlisted() {
assert!(is_serde_json_value_leaf(&parse_ty(
"vespera::serde_json::Value"
)));
}
#[test]
fn unrelated_value_segment_is_not_allowlisted() {
assert!(!is_serde_json_value_leaf(&parse_ty("foo::bar::Value")));
}
#[test]
fn last_segment_not_value_is_not_allowlisted() {
assert!(!is_serde_json_value_leaf(&parse_ty("serde_json::Map")));
}
#[test]
fn serde_json_allowlist_rejects_non_path_and_empty_path_types() {
assert!(!is_serde_json_value_leaf(&parse_ty("&Value")));
assert!(!is_serde_json_value_leaf(&empty_path_type()));
}
#[test]
fn generic_param_directly() {
let mut generics = HashSet::new();
generics.insert("T".to_string());
assert!(type_contains_generic(&parse_ty("T"), &generics));
}
#[test]
fn generic_param_inside_vec() {
let mut generics = HashSet::new();
generics.insert("T".to_string());
assert!(type_contains_generic(&parse_ty("Vec<T>"), &generics));
}
#[test]
fn generic_param_inside_box_inside_option() {
let mut generics = HashSet::new();
generics.insert("T".to_string());
assert!(type_contains_generic(
&parse_ty("Option<Box<HashMap<String, T>>>"),
&generics
));
}
#[test]
fn no_generic_param_present() {
let mut generics = HashSet::new();
generics.insert("T".to_string());
assert!(!type_contains_generic(&parse_ty("Vec<String>"), &generics));
assert!(!type_contains_generic(
&parse_ty("HashMap<String, MyType>"),
&generics
));
}
#[test]
fn empty_generics_set_never_matches() {
let generics = HashSet::new();
assert!(!type_contains_generic(&parse_ty("T"), &generics));
}
#[test]
fn generic_detection_descends_through_references_and_rejects_other_types() {
let generics = HashSet::from(["T".to_string()]);
assert!(type_contains_generic(&parse_ty("&T"), &generics));
assert!(!type_contains_generic(&parse_ty("(T, String)"), &generics));
}
#[test]
fn marker_impl_for_plain_struct() {
let input: syn::DeriveInput = parse_quote! {
pub struct Plain { x: i32 }
};
let out = emit_marker_impl(&input).to_string();
assert!(out.contains("impl :: vespera :: Schema for Plain"));
}
#[test]
fn marker_impl_preserves_generics_and_bounds() {
let input: syn::DeriveInput = parse_quote! {
pub struct Container<T: Serialize> { value: T }
};
let out = emit_marker_impl(&input).to_string();
assert!(out.contains("impl < T : Serialize > :: vespera :: Schema for Container < T >"));
}
#[test]
fn marker_impl_for_enum() {
let input: syn::DeriveInput = parse_quote! {
pub enum E { A, B(i32) }
};
let out = emit_marker_impl(&input).to_string();
assert!(out.contains("impl :: vespera :: Schema for E"));
}
#[test]
fn no_assertion_for_builtin_only_struct() {
let input: syn::DeriveInput = parse_quote! {
pub struct Plain {
id: i32,
name: String,
tags: Vec<String>,
map: HashMap<String, bool>,
}
};
let out = emit_field_assertions(&input, &constraints_for(&input)).to_string();
assert!(
out.is_empty(),
"builtins-only struct must emit zero assertions, got: {out}"
);
}
#[test]
fn assertion_for_unknown_custom_leaf() {
let input: syn::DeriveInput = parse_quote! {
pub struct Holder {
inner: MyUnknown,
}
};
let out = emit_field_assertions(&input, &constraints_for(&input)).to_string();
assert!(
out.contains("__vespera_assert_schema") && out.contains("MyUnknown"),
"expected assertion on MyUnknown, got: {out}"
);
}
#[test]
fn no_assertion_for_serde_json_value() {
let input: syn::DeriveInput = parse_quote! {
pub struct Holder {
raw: serde_json::Value,
bare: Value,
via: Option<Vec<serde_json::Value>>,
}
};
let out = emit_field_assertions(&input, &constraints_for(&input)).to_string();
assert!(
out.is_empty(),
"Value fields must not emit assertions, got: {out}"
);
}
#[test]
fn no_assertion_for_any_marked_field() {
let input: syn::DeriveInput = parse_quote! {
pub struct Holder {
#[schema(any)]
opaque: MyUnknown,
}
};
let out = emit_field_assertions(&input, &constraints_for(&input)).to_string();
assert!(
out.is_empty(),
"`#[schema(any)]` must skip the assertion, got: {out}"
);
}
#[test]
fn no_assertion_for_generic_parameter_leaf() {
let input: syn::DeriveInput = parse_quote! {
pub struct Generic<T> {
value: T,
vec: Vec<T>,
map: HashMap<String, T>,
name: String,
}
};
let out = emit_field_assertions(&input, &constraints_for(&input)).to_string();
assert!(
out.is_empty(),
"fields whose leaf is the struct's generic param must not emit assertions, got: {out}"
);
}
#[test]
fn assertion_per_unknown_field_individually() {
let input: syn::DeriveInput = parse_quote! {
pub struct Holder {
a: AlphaType,
b: Vec<BetaType>,
c: HashMap<String, GammaType>,
d: i32,
#[schema(any)]
e: WhateverType,
}
};
let out = emit_field_assertions(&input, &constraints_for(&input)).to_string();
assert_eq!(
out.matches("let _ = __vespera_assert_schema").count(),
3,
"expected one assertion per non-exempt field: {out}"
);
assert!(out.contains("AlphaType"));
assert!(out.contains("BetaType"));
assert!(out.contains("GammaType"));
assert!(!out.contains("WhateverType"));
}
#[test]
fn no_assertion_for_non_struct_kinds() {
let input: syn::DeriveInput = parse_quote! {
pub enum E { A(MyUnknown) }
};
assert!(
emit_field_assertions(&input, &constraints_for(&input))
.to_string()
.is_empty()
);
let input: syn::DeriveInput = parse_quote! {
pub struct Tuple(MyUnknown);
};
assert!(
emit_field_assertions(&input, &constraints_for(&input))
.to_string()
.is_empty()
);
let input: syn::DeriveInput = parse_quote! {
pub struct Unit;
};
assert!(
emit_field_assertions(&input, &constraints_for(&input))
.to_string()
.is_empty()
);
}
}