Skip to content

Commit 5287252

Browse files
add Deserialize::validate for non-allocating validation (#4493)
# Description of Changes Adds `Deserialize::validate` and all the friends necessary to make that work. The point of this is to enable allocation-less validation of input in some data format, in particular BSATN, against some expected type, in particular an `AlgebraicValue` at an `AlgebraicType`. This is extracted from #4311 and is used there to convert raw BSATN to `BytesKey` without constructing the composite `AlgebraicValue` just for validation only to throw it away immediately. # API and ABI breaking changes None # Expected complexity level and risk 2 # Testing TODO: tweak existing SATS roundtrip tests to also check validation. --------- Co-authored-by: Noa <coolreader18@gmail.com>
1 parent 047dac9 commit 5287252

11 files changed

Lines changed: 875 additions & 108 deletions

File tree

Cargo.lock

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bindings-macro/src/sats.rs

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,8 @@ pub(crate) fn derive_deserialize(ty: &SatsType<'_>) -> TokenStream {
347347
de_generics.params.insert(0, de_lt_param.into());
348348
let (de_impl_generics, _, de_where_clause) = de_generics.split_for_impl();
349349

350-
let (iter_n, iter_n2, iter_n3, iter_n4) = (0usize.., 0usize.., 0usize.., 0usize..);
350+
let (iter_n, iter_n2, iter_n3, iter_n4, iter_n5, iter_n6, iter_n7) =
351+
(0usize.., 0usize.., 0usize.., 0usize.., 0usize.., 0usize.., 0usize..);
351352

352353
match &ty.data {
353354
SatsTypeData::Product(fields) => {
@@ -382,8 +383,10 @@ pub(crate) fn derive_deserialize(ty: &SatsType<'_>) -> TokenStream {
382383

383384
let field_names = fields.iter().map(|f| f.ident.unwrap()).collect::<Vec<_>>();
384385
let field_strings = fields.iter().map(|f| f.name.as_deref().unwrap()).collect::<Vec<_>>();
385-
let field_types = fields.iter().map(|f| &f.ty);
386+
let field_types = fields.iter().map(|f| f.ty);
386387
let field_types2 = field_types.clone();
388+
let field_types3 = field_types.clone();
389+
let field_types4 = field_types.clone();
387390
quote! {
388391
#[allow(non_camel_case_types)]
389392
#[allow(clippy::all)]
@@ -396,6 +399,12 @@ pub(crate) fn derive_deserialize(ty: &SatsType<'_>) -> TokenStream {
396399
_marker: std::marker::PhantomData::<fn() -> #name #ty_generics>,
397400
})
398401
}
402+
403+
fn validate<D: #spacetimedb_lib::de::Deserializer<'de>>(deserializer: D) -> Result<(), D::Error> {
404+
deserializer.validate_product(__ProductVisitor {
405+
_marker: std::marker::PhantomData::<fn() -> #name #ty_generics>,
406+
})
407+
}
399408
}
400409

401410
struct __ProductVisitor #impl_generics #where_clause {
@@ -419,6 +428,13 @@ pub(crate) fn derive_deserialize(ty: &SatsType<'_>) -> TokenStream {
419428
.ok_or_else(|| #spacetimedb_lib::de::Error::invalid_product_length(#iter_n, &self))?,)*
420429
})
421430
}
431+
fn validate_seq_product<A: #spacetimedb_lib::de::SeqProductAccess<'de>>(self, mut tup: A) -> Result<(), A::Error> {
432+
#(
433+
tup.validate_next_element::<#field_types2>()?
434+
.ok_or_else(|| #spacetimedb_lib::de::Error::invalid_product_length(#iter_n2, &self))?;
435+
)*
436+
Ok(())
437+
}
422438
fn visit_named_product<A: #spacetimedb_lib::de::NamedProductAccess<'de>>(self, mut __prod: A) -> Result<Self::Output, A::Error> {
423439
#(let mut #field_names = None;)*
424440
while let Some(__field) = #spacetimedb_lib::de::NamedProductAccess::get_field_ident(&mut __prod, Self {
@@ -427,17 +443,39 @@ pub(crate) fn derive_deserialize(ty: &SatsType<'_>) -> TokenStream {
427443
match __field {
428444
#(__ProductFieldIdent::#field_names => {
429445
if #field_names.is_some() {
430-
return Err(#spacetimedb_lib::de::Error::duplicate_field(#iter_n2, Some(#field_strings), &self))
446+
return Err(#spacetimedb_lib::de::Error::duplicate_field(#iter_n3, Some(#field_strings), &self))
431447
}
432-
#field_names = Some(#spacetimedb_lib::de::NamedProductAccess::get_field_value::<#field_types2>(&mut __prod)?)
448+
#field_names = Some(#spacetimedb_lib::de::NamedProductAccess::get_field_value::<#field_types3>(&mut __prod)?)
433449
})*
434450
}
435451
}
436452
Ok(#name {
437453
#(#field_names:
438-
#field_names.ok_or_else(|| #spacetimedb_lib::de::Error::missing_field(#iter_n3, Some(#field_strings), &self))?,)*
454+
#field_names.ok_or_else(|| #spacetimedb_lib::de::Error::missing_field(#iter_n4, Some(#field_strings), &self))?,)*
439455
})
440456
}
457+
fn validate_named_product<A: #spacetimedb_lib::de::NamedProductAccess<'de>>(self, mut __prod: A) -> Result<(), A::Error> {
458+
#(let mut #field_names = false;)*
459+
while let Some(__field) = #spacetimedb_lib::de::NamedProductAccess::get_field_ident(&mut __prod, Self {
460+
_marker: std::marker::PhantomData,
461+
})? {
462+
match __field {
463+
#(__ProductFieldIdent::#field_names => {
464+
if #field_names {
465+
return Err(#spacetimedb_lib::de::Error::duplicate_field(#iter_n5, Some(#field_strings), &self))
466+
}
467+
#spacetimedb_lib::de::NamedProductAccess::validate_field_value::<#field_types4>(&mut __prod)?;
468+
#field_names = true;
469+
})*
470+
}
471+
}
472+
#(
473+
if !#field_names {
474+
return Err(#spacetimedb_lib::de::Error::missing_field(#iter_n6, Some(#field_strings), &self));
475+
}
476+
)*
477+
Ok(())
478+
}
441479
}
442480

443481
impl #de_impl_generics #spacetimedb_lib::de::FieldNameVisitor<'de> for __ProductVisitor #ty_generics #de_where_clause {
@@ -456,7 +494,7 @@ pub(crate) fn derive_deserialize(ty: &SatsType<'_>) -> TokenStream {
456494

457495
fn visit_seq(self, index: usize) -> Self::Output {
458496
match index {
459-
#(#iter_n4 => __ProductFieldIdent::#field_names,)*
497+
#(#iter_n7 => __ProductFieldIdent::#field_names,)*
460498
_ => core::unreachable!(),
461499
}
462500
}
@@ -488,6 +526,18 @@ pub(crate) fn derive_deserialize(ty: &SatsType<'_>) -> TokenStream {
488526
}
489527
}
490528
});
529+
let arms_validate = variants.iter().map(|var| {
530+
let ident = var.ident;
531+
if let Some(ty) = var.ty {
532+
quote! {
533+
__Variant::#ident => #spacetimedb_lib::de::VariantAccess::validate::<#ty>(__access)?,
534+
}
535+
} else {
536+
quote! {
537+
__Variant::#ident => #spacetimedb_lib::de::VariantAccess::validate::<()>(__access)?,
538+
}
539+
}
540+
});
491541
quote! {
492542
#[allow(clippy::all)]
493543
const _: () = {
@@ -497,6 +547,12 @@ pub(crate) fn derive_deserialize(ty: &SatsType<'_>) -> TokenStream {
497547
_marker: std::marker::PhantomData::<fn() -> #name #ty_generics>,
498548
})
499549
}
550+
551+
fn validate<D: #spacetimedb_lib::de::Deserializer<'de>>(deserializer: D) -> Result<(), D::Error> {
552+
deserializer.validate_sum(__SumVisitor {
553+
_marker: std::marker::PhantomData::<fn() -> #name #ty_generics>,
554+
})
555+
}
500556
}
501557

502558
struct __SumVisitor #impl_generics #where_clause {
@@ -516,6 +572,14 @@ pub(crate) fn derive_deserialize(ty: &SatsType<'_>) -> TokenStream {
516572
#(#arms)*
517573
}
518574
}
575+
576+
fn validate_sum<A: #spacetimedb_lib::de::SumAccess<'de>>(self, __data: A) -> Result<(), A::Error> {
577+
let (__variant, __access) = __data.variant(self)?;
578+
match __variant {
579+
#(#arms_validate)*
580+
}
581+
Ok(())
582+
}
519583
}
520584

521585
#[allow(non_camel_case_types)]

crates/bindings/tests/ui/tables.stderr

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,36 @@ note: required by a bound in `spacetimedb::spacetimedb_lib::de::SeqProductAccess
6363
| fn next_element<T: Deserialize<'de>>(&mut self) -> Result<Option<T>, Self::Error> {
6464
| ^^^^^^^^^^^^^^^^ required by this bound in `SeqProductAccess::next_element`
6565

66+
error[E0277]: the trait bound `Test: Deserialize<'de>` is not satisfied
67+
--> tests/ui/tables.rs:5:8
68+
|
69+
3 | #[spacetimedb::table(accessor = table)]
70+
| --------------------------------------- required by a bound introduced by this call
71+
4 | struct Table {
72+
5 | x: Test,
73+
| ^^^^ unsatisfied trait bound
74+
|
75+
help: the trait `Deserialize<'de>` is not implemented for `Test`
76+
--> tests/ui/tables.rs:1:1
77+
|
78+
1 | struct Test;
79+
| ^^^^^^^^^^^
80+
= help: the following other types implement trait `Deserialize<'de>`:
81+
&'de [u8]
82+
&'de str
83+
()
84+
(T0, T1)
85+
(T0, T1, T2)
86+
(T0, T1, T2, T3)
87+
(T0, T1, T2, T3, T4)
88+
(T0, T1, T2, T3, T4, T5)
89+
and $N others
90+
note: required by a bound in `spacetimedb::spacetimedb_lib::de::SeqProductAccess::validate_next_element`
91+
--> $WORKSPACE/crates/sats/src/de.rs
92+
|
93+
| fn validate_next_element<T: Deserialize<'de>>(&mut self) -> Result<Option<()>, Self::Error> {
94+
| ^^^^^^^^^^^^^^^^ required by this bound in `SeqProductAccess::validate_next_element`
95+
6696
error[E0277]: the trait bound `Test: Deserialize<'_>` is not satisfied
6797
--> tests/ui/tables.rs:5:8
6898
|
@@ -90,6 +120,33 @@ note: required by a bound in `get_field_value`
90120
| fn get_field_value<T: Deserialize<'de>>(&mut self) -> Result<T, Self::Error> {
91121
| ^^^^^^^^^^^^^^^^ required by this bound in `NamedProductAccess::get_field_value`
92122

123+
error[E0277]: the trait bound `Test: Deserialize<'_>` is not satisfied
124+
--> tests/ui/tables.rs:5:8
125+
|
126+
5 | x: Test,
127+
| ^^^^ unsatisfied trait bound
128+
|
129+
help: the trait `Deserialize<'_>` is not implemented for `Test`
130+
--> tests/ui/tables.rs:1:1
131+
|
132+
1 | struct Test;
133+
| ^^^^^^^^^^^
134+
= help: the following other types implement trait `Deserialize<'de>`:
135+
&'de [u8]
136+
&'de str
137+
()
138+
(T0, T1)
139+
(T0, T1, T2)
140+
(T0, T1, T2, T3)
141+
(T0, T1, T2, T3, T4)
142+
(T0, T1, T2, T3, T4, T5)
143+
and $N others
144+
note: required by a bound in `validate_field_value`
145+
--> $WORKSPACE/crates/sats/src/de.rs
146+
|
147+
| fn validate_field_value<T: Deserialize<'de>>(&mut self) -> Result<(), Self::Error> {
148+
| ^^^^^^^^^^^^^^^^ required by this bound in `NamedProductAccess::validate_field_value`
149+
93150
error[E0277]: the trait bound `Test: Serialize` is not satisfied
94151
--> tests/ui/tables.rs:5:8
95152
|

0 commit comments

Comments
 (0)