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 empty = Constraints::default();
let constraints = field.constraints.as_ref().unwrap_or(&empty);
let tests = if crate::lower::constraints::accepts_only_null(field) {
vec![Check {
test: quote! {{ let _ = item; true }},
message: "has no non-null value within the declared bounds".to_owned(),
}]
} else {
checks(constraints, checked_type(field, constraints))
};
let body = wrap(&field.ty, &tests, field.name.logical(), constraints.checked_as.as_ref());
let read = match &field.ty {
RustType::Option(inner) => {
let inner = emit_type(inner)?;
quote! { Some(<#inner as serde::Deserialize>::deserialize(deserializer)?) }
}
_ => quote! { <#ty as serde::Deserialize>::deserialize(deserializer)? },
};
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 = #read;
#body
return Ok(value);
}
});
}
struct Check {
test: TokenStream,
message: String,
}
fn wrap(ty: &RustType, tests: &[Check], label: &str, checked_as: Option<&RustType>) -> TokenStream {
if tests.is_empty() {
return quote! {};
}
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();
let body = unwrap_checks(ty, quote! { #(#rules)* }, checked_as);
return quote! {{ let item = &value; #body }};
}
fn unwrap_checks(ty: &RustType, body: TokenStream, checked_as: Option<&RustType>) -> TokenStream {
return match ty {
RustType::Option(inner) | RustType::Nullable(inner) => {
let body = unwrap_checks(inner, body, checked_as);
quote! { if let Some(item) = item.as_ref() { #body } }
}
RustType::Boxed(inner) => {
let body = unwrap_checks(inner, body, checked_as);
quote! {{ let item = item.as_ref(); #body }}
}
RustType::Named(_) => match checked_as {
Some(checked) => unwrap_checks(checked, body, None),
_ => body,
},
_ => body,
};
}
fn checked_type<'a>(field: &'a Field, constraints: &'a Constraints) -> &'a RustType {
return match &constraints.checked_as {
Some(ty) => ty.innermost(),
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.innermost(), 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(),
};
}