use proc_macro2::TokenStream;
use syn::DeriveInput;
use crate::parser::schema::schema_attrs::SchemaConstraints;
#[cfg(feature = "validation")]
use proc_macro2::Span;
#[cfg(feature = "validation")]
use quote::{format_ident, quote};
#[cfg(feature = "validation")]
use syn::{Data, Fields, Type};
#[cfg(feature = "validation")]
#[must_use]
pub fn emit_garde_validate(
input: &DeriveInput,
field_constraints: &[SchemaConstraints],
) -> TokenStream {
emit_impl(input, field_constraints)
}
#[cfg(not(feature = "validation"))]
#[must_use]
pub fn emit_garde_validate(
_input: &DeriveInput,
_field_constraints: &[SchemaConstraints],
) -> TokenStream {
TokenStream::new()
}
#[cfg(feature = "validation")]
fn emit_impl(input: &DeriveInput, field_constraints: &[SchemaConstraints]) -> TokenStream {
let Data::Struct(data_struct) = &input.data else {
return TokenStream::new();
};
let Fields::Named(fields_named) = &data_struct.fields else {
return TokenStream::new();
};
let per_field: Vec<(&syn::Field, &SchemaConstraints)> =
fields_named.named.iter().zip(field_constraints).collect();
if per_field.iter().all(|(_, c)| !c.has_runtime_rule()) {
return TokenStream::new();
}
if !input.generics.params.is_empty() {
let msg = format!(
"vespera::Schema validation does not yet support generic / \
lifetime-parameterised types (struct `{}`). Move the \
`#[schema(...)]` constraints to a non-generic wrapper, or \
open an issue if you need this.",
input.ident,
);
return quote! { ::std::compile_error!(#msg); };
}
let struct_ident = &input.ident;
let field_idents: Vec<&syn::Ident> = fields_named
.named
.iter()
.filter_map(|f| f.ident.as_ref())
.collect();
let field_blocks: Vec<TokenStream> = per_field
.iter()
.filter_map(|(field, constraints)| {
let ident = field.ident.as_ref()?;
emit_field_block(ident, &field.ty, constraints)
})
.collect();
if field_blocks.is_empty() {
return TokenStream::new();
}
quote! {
#[allow(
clippy::all,
clippy::pedantic,
clippy::nursery,
unused_variables,
unused_mut,
unused_parens,
non_upper_case_globals,
)]
impl ::vespera::__validation::garde::Validate for #struct_ident {
type Context = ();
fn validate_into(
&self,
__garde_user_ctx: &Self::Context,
mut __garde_path: &mut dyn ::core::ops::FnMut() -> ::vespera::__validation::garde::Path,
__garde_report: &mut ::vespera::__validation::garde::Report,
) {
let _ = __garde_user_ctx; let Self { #(#field_idents),* } = self;
#(#field_blocks)*
}
}
}
}
#[cfg(feature = "validation")]
fn emit_field_block(
field_ident: &syn::Ident,
field_ty: &Type,
c: &SchemaConstraints,
) -> Option<TokenStream> {
if !c.has_runtime_rule() {
return None;
}
let field_name_str = field_ident.to_string();
let numeric_kind = rust_numeric_kind(
crate::schema_macro::type_utils::option_inner(field_ty).unwrap_or(field_ty),
);
let rule_blocks = emit_rule_blocks(c, &field_name_str, numeric_kind.as_deref());
let dive_block = emit_dive_block(c);
if rule_blocks.is_empty() && dive_block.is_empty() {
return None;
}
let block = if crate::schema_macro::type_utils::is_option_type(field_ty) {
quote! {
{
let mut __garde_path = ::vespera::__validation::garde::util::nested_path!(
__garde_path, #field_name_str
);
if let ::std::option::Option::Some(__garde_binding) = #field_ident {
#rule_blocks
#dive_block
}
}
}
} else {
quote! {
{
let mut __garde_path = ::vespera::__validation::garde::util::nested_path!(
__garde_path, #field_name_str
);
let __garde_binding = &*#field_ident;
#rule_blocks
#dive_block
}
}
};
Some(block)
}
#[cfg(feature = "validation")]
fn emit_dive_block(c: &SchemaConstraints) -> TokenStream {
if c.dive == Some(true) {
quote! {
::vespera::__validation::garde::Validate::validate_into(
&*__garde_binding,
__garde_user_ctx,
&mut __garde_path,
__garde_report,
);
}
} else {
TokenStream::new()
}
}
#[cfg(feature = "validation")]
#[allow(clippy::too_many_lines)] fn emit_rule_blocks(
c: &SchemaConstraints,
field_name: &str,
numeric_kind: Option<&str>,
) -> TokenStream {
let mut blocks: Vec<TokenStream> = Vec::new();
if c.min_length.is_some() || c.max_length.is_some() {
let min = c.min_length.unwrap_or(0);
let max = c.max_length.unwrap_or(usize::MAX);
blocks.push(quote! {
if let ::std::result::Result::Err(__garde_error) =
(::vespera::__validation::garde::rules::length::chars::apply)(
&*__garde_binding,
(#min, #max),
)
{
__garde_report.append(__garde_path(), __garde_error);
}
});
}
if c.min_items.is_some() || c.max_items.is_some() {
let min = c.min_items.unwrap_or(0);
let max = c.max_items.unwrap_or(usize::MAX);
blocks.push(quote! {
if let ::std::result::Result::Err(__garde_error) =
(::vespera::__validation::garde::rules::length::simple::apply)(
&*__garde_binding,
(#min, #max),
)
{
__garde_report.append(__garde_path(), __garde_error);
}
});
}
if c.minimum.is_some() || c.maximum.is_some() {
let min_expr = numeric_some(c.minimum, numeric_kind);
let max_expr = numeric_some(c.maximum, numeric_kind);
blocks.push(quote! {
if let ::std::result::Result::Err(__garde_error) =
(::vespera::__validation::garde::rules::range::apply)(
__garde_binding,
(#min_expr, #max_expr),
)
{
__garde_report.append(__garde_path(), __garde_error);
}
});
}
if let Some(pattern) = &c.pattern {
if let Err(__err) = regex_syntax::Parser::new().parse(pattern) {
let msg = format!(
"vespera: `#[schema(pattern = {pattern:?})]` on field `{field_name}` is not a valid regex: {__err}"
);
blocks.push(quote! { ::std::compile_error!(#msg); });
} else {
let ident_fragment: String = field_name
.trim_start_matches("r#")
.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() {
ch.to_ascii_uppercase()
} else {
'_'
}
})
.collect();
let static_ident = format_ident!("__VESPERA_PATTERN_{}", ident_fragment);
blocks.push(quote! {
{
static #static_ident: ::std::sync::LazyLock<
::vespera::__validation::garde::rules::pattern::regex::Regex,
> = ::std::sync::LazyLock::new(|| {
::vespera::__validation::garde::rules::pattern::regex::Regex::new(#pattern)
.unwrap_or_else(|__e| {
::std::panic!(
"vespera: `#[schema(pattern = {:?})]` is not a valid regex: {__e}",
#pattern
)
})
});
if let ::std::result::Result::Err(__garde_error) =
(::vespera::__validation::garde::rules::pattern::apply)(
&*__garde_binding,
(&*#static_ident,),
)
{
__garde_report.append(__garde_path(), __garde_error);
}
}
});
}
}
if let Some(fmt) = c.format.as_deref() {
match fmt {
"email" => blocks.push(quote! {
if let ::std::result::Result::Err(__garde_error) =
(::vespera::__validation::garde::rules::email::apply)(
&*__garde_binding,
(),
)
{
__garde_report.append(__garde_path(), __garde_error);
}
}),
"uri" | "url" => blocks.push(quote! {
if let ::std::result::Result::Err(__garde_error) =
(::vespera::__validation::garde::rules::url::apply)(
&*__garde_binding,
(),
)
{
__garde_report.append(__garde_path(), __garde_error);
}
}),
"ipv4" => blocks.push(quote! {
if let ::std::result::Result::Err(__garde_error) =
(::vespera::__validation::garde::rules::ip::apply)(
&*__garde_binding,
(::vespera::__validation::garde::rules::ip::IpKind::V4,),
)
{
__garde_report.append(__garde_path(), __garde_error);
}
}),
"ipv6" => blocks.push(quote! {
if let ::std::result::Result::Err(__garde_error) =
(::vespera::__validation::garde::rules::ip::apply)(
&*__garde_binding,
(::vespera::__validation::garde::rules::ip::IpKind::V6,),
)
{
__garde_report.append(__garde_path(), __garde_error);
}
}),
"ip" => blocks.push(quote! {
if let ::std::result::Result::Err(__garde_error) =
(::vespera::__validation::garde::rules::ip::apply)(
&*__garde_binding,
(::vespera::__validation::garde::rules::ip::IpKind::Any,),
)
{
__garde_report.append(__garde_path(), __garde_error);
}
}),
_ => {}
}
}
quote! { #(#blocks)* }
}
#[cfg(feature = "validation")]
fn numeric_some(value: Option<f64>, numeric_kind: Option<&str>) -> TokenStream {
let Some(v) = value else {
return quote! { ::std::option::Option::None };
};
numeric_kind.map_or_else(
|| quote! { ::std::option::Option::Some(#v as _) },
|kind| {
let ty_ident = syn::Ident::new(kind, Span::call_site());
let is_float = matches!(kind, "f32" | "f64");
if !is_float && v.fract() == 0.0 && v.is_finite() {
#[allow(clippy::cast_possible_truncation)]
let i = v as i64;
quote! { ::std::option::Option::Some(#i as #ty_ident) }
} else {
quote! { ::std::option::Option::Some(#v as #ty_ident) }
}
},
)
}
#[cfg(feature = "validation")]
fn rust_numeric_kind(ty: &Type) -> Option<String> {
let Type::Path(tp) = ty else {
return None;
};
let last = tp.path.segments.last()?;
let name = last.ident.to_string();
matches!(
name.as_str(),
"i8" | "i16"
| "i32"
| "i64"
| "i128"
| "isize"
| "u8"
| "u16"
| "u32"
| "u64"
| "u128"
| "usize"
| "f32"
| "f64"
)
.then_some(name)
}
#[cfg(all(test, feature = "validation"))]
mod tests;