use proc_macro2::TokenStream;
use quote::format_ident;
use quote::quote;
use super::emit_type;
use crate::error::Result;
use crate::ir::Bound;
use crate::ir::Constraints;
use crate::ir::Field;
use crate::ir::RustType;
pub(crate) fn validate_fn_name(field: &Field) -> proc_macro2::Ident {
return format_ident!("validate_{}", field.name.logical());
}
pub(crate) fn is_checked(field: &Field) -> bool {
let Some(constraints) = &field.constraints else {
return false;
};
return !checks(constraints, checked_type(field, constraints)).is_empty();
}
pub(crate) fn emit_validate_fn(field: &Field) -> Result<TokenStream> {
let name = validate_fn_name(field);
let ty = emit_type(&field.ty)?;
let Some(constraints) = &field.constraints else {
return Ok(quote! {});
};
let tests = checks(constraints, checked_type(field, constraints));
let body = wrap(&field.ty, &tests, field.name.logical());
let pattern = emit_pattern(field, constraints)?;
let doc = format!(
" The rules the document gives `{}`, checked on the way in.",
field.name.logical()
);
return Ok(quote! {
#[doc = #doc]
fn #name<'de, D>(deserializer: D) -> ::core::result::Result<#ty, D::Error>
where
D: serde::Deserializer<'de>,
{
#pattern
let value = <#ty as serde::Deserialize>::deserialize(deserializer)?;
#body
return Ok(value);
}
});
}
struct Check {
test: TokenStream,
message: String,
}
fn wrap(ty: &RustType, tests: &[Check], label: &str) -> TokenStream {
let rules = tests.iter().map(|check| {
let test = &check.test;
let message = format!("`{label}` {}", check.message);
return quote! {
if #test {
return Err(serde::de::Error::custom(#message));
}
};
});
let rules: Vec<TokenStream> = rules.collect();
if ty.is_option() {
return quote! {
if let Some(item) = value.as_ref() {
#(#rules)*
}
};
}
return quote! {
{
let item = &value;
#(#rules)*
}
};
}
fn checked_type<'a>(field: &'a Field, constraints: &'a Constraints) -> &'a RustType {
return match &constraints.checked_as {
Some(ty) => ty,
None => field.ty.innermost(),
};
}
fn checks(constraints: &Constraints, ty: &RustType) -> Vec<Check> {
let mut tests = Vec::new();
if matches!(*ty, RustType::String) {
string_checks(constraints, &mut tests);
}
if matches!(
*ty,
RustType::I32 | RustType::I64 | RustType::U32 | RustType::U64 | RustType::F64
) {
number_checks(constraints, ty, &mut tests);
}
if matches!(*ty, RustType::Vec(_)) {
array_checks(constraints, ty, &mut tests);
}
if matches!(*ty, RustType::Map(_)) {
map_checks(constraints, &mut tests);
}
return tests;
}
fn emit_pattern(field: &Field, constraints: &Constraints) -> Result<TokenStream> {
let Some(pattern) = &constraints.pattern else {
return Ok(quote! {});
};
if !matches!(*checked_type(field, constraints), RustType::String) {
return Ok(quote! {});
}
if let Err(problem) = regex::Regex::new(pattern) {
return Err(crate::error::Error::UnsupportedSchema {
path: field.name.logical().to_owned(),
reason: format!("the `pattern` `{pattern}` does not read as a regular expression: {problem}"),
});
}
let note = format!("the generator read `{pattern}` at generation time");
return Ok(quote! {
static PATTERN: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
let pattern = PATTERN.get_or_init(|| return regex::Regex::new(#pattern).expect(#note));
});
}
fn string_checks(constraints: &Constraints, tests: &mut Vec<Check>) {
if let Some(pattern) = &constraints.pattern {
tests.push(Check {
test: quote! { !pattern.is_match(item) },
message: format!("must match `{pattern}`"),
});
}
if let Some(min) = constraints.min_length.filter(|min| return *min > 0) {
let last = min - 1;
tests.push(Check {
test: quote! { item.chars().nth(#last).is_none() },
message: format!("must hold {min} or more characters"),
});
}
if let Some(max) = constraints.max_length {
tests.push(Check {
test: quote! { item.chars().nth(#max).is_some() },
message: format!("must hold {max} or fewer characters"),
});
}
}
fn type_limits(ty: &RustType) -> (Option<i64>, Option<i64>) {
return match *ty {
RustType::I32 => (Some(i64::from(i32::MIN)), Some(i64::from(i32::MAX))),
RustType::I64 => (Some(i64::MIN), Some(i64::MAX)),
RustType::U32 => (Some(0), Some(i64::from(u32::MAX))),
RustType::U64 => (Some(0), None),
_ => (None, None),
};
}
fn number_checks(constraints: &Constraints, ty: &RustType, tests: &mut Vec<Check>) {
let (lowest, highest) = type_limits(ty);
if let Some(min) = constraints.minimum {
let literal = bound_literal(min);
let text = bound_text(min);
let no_value_fails = !constraints.exclusive_minimum
&& matches!(min, Bound::Int(value) if lowest.is_some_and(|lowest| return value <= lowest));
let test = if constraints.exclusive_minimum {
Some(Check {
test: quote! { *item <= #literal },
message: format!("must be more than {text}"),
})
} else if no_value_fails {
None
} else {
Some(Check {
test: quote! { *item < #literal },
message: format!("must be {text} or more"),
})
};
if let Some(check) = test {
tests.push(check);
}
}
if let Some(max) = constraints.maximum {
let literal = bound_literal(max);
let text = bound_text(max);
let no_value_fails = !constraints.exclusive_maximum
&& matches!(max, Bound::Int(value) if highest.is_some_and(|highest| return value >= highest));
let test = if constraints.exclusive_maximum {
Some(Check {
test: quote! { *item >= #literal },
message: format!("must be less than {text}"),
})
} else if no_value_fails {
None
} else {
Some(Check {
test: quote! { *item > #literal },
message: format!("must be {text} or less"),
})
};
if let Some(check) = test {
tests.push(check);
}
}
if let Some(step) = constraints.multiple_of {
let literal = bound_literal(step);
let text = bound_text(step);
if matches!(*ty, RustType::F64) {
tests.push(Check {
test: quote! {{
let steps = *item / #literal;
(steps - steps.round()).abs() > f64::EPSILON * steps.abs().max(1.0) * 8.0
}},
message: format!("must be a multiple of {text}"),
});
} else {
tests.push(Check {
test: quote! { *item % #literal != 0 },
message: format!("must be a multiple of {text}"),
});
}
}
}
fn array_checks(constraints: &Constraints, ty: &RustType, tests: &mut Vec<Check>) {
if let Some(min) = constraints.min_items {
tests.push(Check {
test: quote! { item.len() < #min },
message: format!("must hold {min} or more items"),
});
}
if let Some(max) = constraints.max_items {
tests.push(Check {
test: quote! { item.len() > #max },
message: format!("must hold {max} or fewer items"),
});
}
if !constraints.unique_items {
return;
}
let RustType::Vec(ref element) = *ty else {
return;
};
if !element.is_scalar() {
return;
}
let test = if matches!(**element, RustType::F64) {
quote! {
item.iter().enumerate().any(|(index, left)| {
return item.iter().skip(index + 1).any(|right| return left == right);
})
}
} else {
quote! {{
let mut seen = std::collections::HashSet::with_capacity(item.len());
item.iter().any(|entry| return !seen.insert(entry))
}}
};
tests.push(Check {
test,
message: "must not repeat an item".to_owned(),
});
}
fn map_checks(constraints: &Constraints, tests: &mut Vec<Check>) {
if let Some(min) = constraints.min_properties {
tests.push(Check {
test: quote! { item.len() < #min },
message: format!("must hold {min} or more properties"),
});
}
if let Some(max) = constraints.max_properties {
tests.push(Check {
test: quote! { item.len() > #max },
message: format!("must hold {max} or fewer properties"),
});
}
}
fn bound_literal(bound: Bound) -> TokenStream {
return match bound {
Bound::Float(value) => {
let literal = proc_macro2::Literal::f64_suffixed(value);
quote! { #literal }
}
Bound::Int(value) => {
let literal = proc_macro2::Literal::i64_unsuffixed(value);
quote! { #literal }
}
};
}
fn bound_text(bound: Bound) -> String {
return match bound {
Bound::Int(value) => value.to_string(),
Bound::Float(value) => value.to_string(),
};
}