use crate::{imp::field_walk::field_walk_bindings, prelude::*};
use quote::format_ident;
pub struct ValidateAutoTrait;
pub trait ValidateAutoFn {
fn self_tokens(_: &Self) -> TokenStream {
quote!()
}
}
macro_rules! impl_validate_auto {
($($ty:ty),* $(,)?) => {
$(impl Imp<$ty> for ValidateAutoTrait {
fn strategy(node: &$ty) -> Option<TraitStrategy> {
let self_tokens = ValidateAutoFn::self_tokens(node);
let tokens = Implementor::new(node.def(), TraitKind::ValidateAuto)
.add_tokens(self_tokens)
.to_token_stream();
Some(TraitStrategy::from_impl(tokens))
}
})*
};
}
impl_validate_auto!(Enum, List, Map, Newtype, Set);
impl Imp<Entity> for ValidateAutoTrait {
fn strategy(node: &Entity) -> Option<TraitStrategy> {
Some(field_list_validate_strategy(node.def(), &node.fields))
}
}
impl Imp<Record> for ValidateAutoTrait {
fn strategy(node: &Record) -> Option<TraitStrategy> {
Some(field_list_validate_strategy(node.def(), &node.fields))
}
}
impl ValidateAutoFn for Enum {}
impl ValidateAutoFn for List {
fn self_tokens(node: &Self) -> TokenStream {
collection_validate_self_tokens(&node.ty.validators, &node.item.validators)
}
}
impl ValidateAutoFn for Map {
fn self_tokens(node: &Self) -> TokenStream {
let map_rules =
generate_validators_inner(&node.ty.validators, quote!(&self.0), quote!(ctx));
let key_rules =
generate_validators_inner(&node.key.validators, quote!(k), quote!(&mut entry_ctx));
let value_rules =
generate_value_validation_inner(&node.value, quote!(v), quote!(&mut entry_ctx));
let entry_rules = match (key_rules, value_rules) {
(None, None) => None,
(k, v) => {
let k = k.unwrap_or_default();
let v = v.unwrap_or_default();
Some(quote! {
for (i, (k, v)) in self.0.iter().enumerate() {
let mut entry_ctx = ::icydb_model::visitor::ScopedContext::new(
ctx,
::icydb_model::visitor::PathSegment::Index(i),
);
#k
#v
}
})
}
};
wrap_validate_self_fn(merge_rules(map_rules, entry_rules))
}
}
impl ValidateAutoFn for Newtype {
fn self_tokens(node: &Self) -> TokenStream {
newtype_validate_self_tokens(node)
}
}
impl ValidateAutoFn for Set {
fn self_tokens(node: &Self) -> TokenStream {
collection_validate_self_tokens(&node.ty.validators, &node.item.validators)
}
}
fn merge_rules(a: Option<TokenStream>, b: Option<TokenStream>) -> Option<TokenStream> {
match (a, b) {
(None, None) => None,
(x, None) => x,
(None, y) => y,
(Some(x), Some(y)) => Some(quote! { #x #y }),
}
}
fn field_list_validate_strategy(def: &Def, fields: &FieldList) -> TraitStrategy {
TraitStrategy::from_impl(field_list(def, fields))
}
fn indexed_collection_item_rules(block: TokenStream) -> TokenStream {
let item_ident = format_ident!("__item");
quote! {
use ::icydb_model::Collection;
for (i, #item_ident) in self.iter().enumerate() {
let item = #item_ident;
let mut item_ctx = ::icydb_model::visitor::ScopedContext::new(
ctx,
::icydb_model::visitor::PathSegment::Index(i),
);
#block
}
}
}
fn collection_validate_self_tokens(
container_validators: &[TypeValidator],
item_validators: &[TypeValidator],
) -> TokenStream {
let container_rules =
generate_validators_inner(container_validators, quote!(&self.0), quote!(ctx));
let item_rules =
generate_validators_inner(item_validators, quote!(item), quote!(&mut item_ctx))
.map(indexed_collection_item_rules);
wrap_validate_self_fn(merge_rules(container_rules, item_rules))
}
fn field_list(def: &Def, fields: &FieldList) -> TokenStream {
let bindings = field_walk_bindings(fields);
let field_table_ident = format_ident!("__VALIDATE_FIELDS");
let validate_helpers = fields
.iter()
.zip(bindings.iter())
.filter_map(|(field, binding)| {
let validation = generate_field_value_validation_inner(
&field.value,
binding.member_ref_from(quote!(node)),
binding.path_segment(),
)?;
let fn_ident = binding.validate_fn_ident();
Some(quote! {
fn #fn_ident(
node: &Self,
ctx: &mut dyn ::icydb_model::visitor::VisitorContext,
) {
#validation
}
})
});
let descriptors = bindings
.iter()
.zip(fields.iter())
.filter_map(|(binding, field)| {
if field.value.item.validators.is_empty() {
None
} else {
let validate_fn = binding.validate_fn_ident();
Some(quote! {
::icydb_model::visitor::ValidateFieldDescriptor::new(Self::#validate_fn)
})
}
});
let inherent_tokens = Implementor::new(def, TraitKind::Inherent)
.set_tokens(quote! {
#(#validate_helpers)*
const #field_table_ident: &'static [::icydb_model::visitor::ValidateFieldDescriptor<Self>] =
&[#(#descriptors),*];
})
.to_token_stream();
let trait_tokens = Implementor::new(def, TraitKind::ValidateAuto)
.set_tokens(wrap_validate_self_fn(Some(quote! {
::icydb_model::visitor::drive_validate_fields(self, ctx, Self::#field_table_ident);
})))
.to_token_stream();
quote! {
#inherent_tokens
#trait_tokens
}
}
fn generate_validators_inner(
validators: &[TypeValidator],
var_expr: TokenStream,
ctx_expr: TokenStream,
) -> Option<TokenStream> {
if validators.is_empty() {
return None;
}
let exprs: Vec<TokenStream> = validators
.iter()
.map(|validator| validator_call(validator, var_expr.clone(), ctx_expr.clone()))
.collect();
Some(quote!(#(#exprs)*))
}
fn newtype_validate_self_tokens(node: &Newtype) -> TokenStream {
let type_rules = generate_validators_inner(&node.ty.validators, quote!(&self.0), quote!(ctx));
let item_rules = generate_validators_inner(&node.item.validators, quote!(&self.0), quote!(ctx));
wrap_validate_self_fn(merge_rules(type_rules, item_rules))
}
fn wrap_validate_self_fn(inner: Option<TokenStream>) -> TokenStream {
match inner {
None => quote!(),
Some(inner) => quote! {
fn validate_self(&self, ctx: &mut dyn ::icydb_model::visitor::VisitorContext) {
#inner
}
},
}
}
fn cardinality_wrapper(
card: Cardinality,
rules: Vec<TokenStream>,
var_expr: TokenStream,
) -> Option<TokenStream> {
if rules.is_empty() {
return None;
}
let body = quote! { #(#rules)* };
let tokens = match card {
Cardinality::One => quote! {
let v = #var_expr;
#body
},
Cardinality::Opt => quote! {
if let Some(v) = #var_expr {
#body
}
},
Cardinality::Many => {
let item = format_ident!("__item");
quote! {
for #item in #var_expr {
let v = #item;
#body
}
}
}
};
Some(tokens)
}
fn generate_value_validation_inner(
value: &Value,
var_expr: TokenStream,
ctx_expr: TokenStream,
) -> Option<TokenStream> {
let rules: Vec<TokenStream> = value
.item
.validators
.iter()
.map(|validator| validator_call(validator, quote!(v), ctx_expr.clone()))
.collect();
cardinality_wrapper(value.cardinality(), rules, var_expr)
}
fn generate_field_value_validation_inner(
value: &Value,
var_expr: TokenStream,
seg: TokenStream,
) -> Option<TokenStream> {
let rules: Vec<TokenStream> = value
.item
.validators
.iter()
.map(|validator| validator_call(validator, quote!(v), quote!(&mut __field_ctx)))
.collect();
let body = cardinality_wrapper(value.cardinality(), rules, var_expr)?;
Some(quote! {{
let mut __field_ctx =
::icydb_model::visitor::ScopedContext::new(ctx, #seg);
#body
}})
}
fn validator_call(
validator: &TypeValidator,
value: TokenStream,
context: TokenStream,
) -> TokenStream {
let ctor = validator.quote_constructor();
let callback_type = &validator.path;
quote! {{
let mut __callback_ctx = ::icydb_model::visitor::CallbackContext::new(
#context,
::icydb_model::visitor::CallbackIdentity::new(
::icydb_model::visitor::CallbackKind::Validator,
::core::any::type_name::<#callback_type>(),
),
);
::icydb_model::visitor::Validator::validate(
&(#ctor),
#value,
&mut __callback_ctx,
);
}}
}