cultcache-rs-derive: arm keys are Int32 and given once, an arm is a slots record, refusals classified like the reference
Key attributes (variant, field, entry type/schema) are refused when repeated. A CultUnion key parses as the reference's Int32, negatives included; a key outside it is a compile error. Decode classifies the key as the reference does: non-integer, outside Int32, undeclared (named), and a pair of the wrong length is refused before its key is read; no value is echoed beyond the undeclared key the reference names. The SlotRecord trait, implemented by the shared slot generator, bounds an arm payload, so a String or Option arm is a compile error. compile_fail fixtures use keys other than 0; the derive crate's unit tests assert each refusal's message. Unverified: committed before the first build; the verification job runs next. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e72f17421e
commit
c586640e81
4 changed files with 460 additions and 29 deletions
|
|
@ -189,7 +189,7 @@ without pretending Rust has runtime reflection hiding under the floorboards.
|
|||
A union that crosses runtimes is written `[armKey, [armSlots...]]`, the shape the C# reference writes for
|
||||
`[Union(key, type)]`. Derive `CultUnion` on an enum: every variant carries `#[cultcache(key = N)]` and is a unit
|
||||
(`[N, []]`) or a newtype over a `#[derive(CultSlots)]` struct, which is a `DatabaseEntry`'s slot array without the
|
||||
entry consts. A key the enum does not declare is a decode error naming the enum and the declared keys, and never
|
||||
entry consts (a `String` or an `Option` is not an arm, and is a compile error). An arm key is the reference's Int32, negatives included, and is given once. A key the enum does not declare is a decode error naming the enum and the declared keys, and never
|
||||
quoting the payload. A null union is `Option<Enum>`. Plain serde enums are untouched and keep their bytes.
|
||||
|
||||
```rust
|
||||
|
|
|
|||
|
|
@ -23,9 +23,11 @@ pub fn derive_database_entry(input: TokenStream) -> TokenStream {
|
|||
{
|
||||
let parse_result = attribute.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("type") {
|
||||
once(&entry_type, &meta, "type")?;
|
||||
entry_type = Some(meta.value()?.parse()?);
|
||||
Ok(())
|
||||
} else if meta.path.is_ident("schema") {
|
||||
once(&schema_name, &meta, "schema")?;
|
||||
schema_name = Some(meta.value()?.parse()?);
|
||||
Ok(())
|
||||
} else {
|
||||
|
|
@ -131,6 +133,7 @@ fn named_slots(data: Data, ident: &syn::Ident, owner: &str) -> syn::Result<Vec<C
|
|||
{
|
||||
attribute.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("key") {
|
||||
once(&key, &meta, "key")?;
|
||||
let value: LitInt = meta.value()?.parse()?;
|
||||
key = Some(value.base10_parse()?);
|
||||
Ok(())
|
||||
|
|
@ -258,6 +261,8 @@ fn slot_codec(ident: &syn::Ident, slots: &[CultCacheSlot]) -> proc_macro2::Token
|
|||
});
|
||||
|
||||
quote! {
|
||||
impl ::cultcache_rs::SlotRecord for #ident {}
|
||||
|
||||
impl ::serde::Serialize for #ident {
|
||||
fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
|
|
@ -310,6 +315,31 @@ fn slot_codec(ident: &syn::Ident, slots: &[CultCacheSlot]) -> proc_macro2::Token
|
|||
}
|
||||
}
|
||||
|
||||
/// An attribute names each thing once; a second `key = N` would silently win otherwise.
|
||||
fn once<T>(seen: &Option<T>, meta: &syn::meta::ParseNestedMeta, name: &str) -> syn::Result<()> {
|
||||
if seen.is_some() {
|
||||
return Err(meta.error(format!("repeated `{name}`: #[cultcache({name} = ...)] is given once")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// An arm key is the reference's Int32, negatives included: `key = 5` or `key = -1`.
|
||||
fn arm_key(meta: &syn::meta::ParseNestedMeta) -> syn::Result<i32> {
|
||||
let expr: syn::Expr = meta.value()?.parse()?;
|
||||
let (negative, literal) = match expr {
|
||||
syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(literal), .. }) => (false, literal),
|
||||
syn::Expr::Unary(syn::ExprUnary { op: syn::UnOp::Neg(_), expr, .. }) => match *expr {
|
||||
syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(literal), .. }) => (true, literal),
|
||||
other => return Err(syn::Error::new_spanned(other, "a CultUnion arm key is an integer literal")),
|
||||
},
|
||||
other => return Err(syn::Error::new_spanned(other, "a CultUnion arm key is an integer literal")),
|
||||
};
|
||||
let magnitude: i128 = literal.base10_parse()?;
|
||||
let value = if negative { -magnitude } else { magnitude };
|
||||
i32::try_from(value)
|
||||
.map_err(|_| syn::Error::new_spanned(&literal, format!("CultUnion arm key {value} is outside Int32")))
|
||||
}
|
||||
|
||||
struct CultCacheSlot {
|
||||
key: usize,
|
||||
ident: syn::Ident,
|
||||
|
|
@ -334,13 +364,13 @@ fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
|
|||
}
|
||||
|
||||
struct Arm {
|
||||
key: u64,
|
||||
key: i32,
|
||||
ident: syn::Ident,
|
||||
payload: Option<Type>,
|
||||
}
|
||||
let mut arms: Vec<Arm> = Vec::new();
|
||||
for variant in data.variants {
|
||||
let mut key: Option<u64> = None;
|
||||
let mut key: Option<i32> = None;
|
||||
for attribute in variant
|
||||
.attrs
|
||||
.iter()
|
||||
|
|
@ -348,8 +378,8 @@ fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
|
|||
{
|
||||
attribute.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("key") {
|
||||
let value: LitInt = meta.value()?.parse()?;
|
||||
key = Some(value.base10_parse()?);
|
||||
once(&key, &meta, "key")?;
|
||||
key = Some(arm_key(&meta)?);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(meta.error("unsupported variant cultcache attribute"))
|
||||
|
|
@ -388,13 +418,16 @@ fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
|
|||
}
|
||||
|
||||
let enum_name = ident.to_string();
|
||||
let declared = arms
|
||||
.iter()
|
||||
.map(|arm| arm.key.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let mut declared_keys: Vec<i32> = arms.iter().map(|arm| arm.key).collect();
|
||||
declared_keys.sort_unstable();
|
||||
let declared = declared_keys.iter().map(i32::to_string).collect::<Vec<_>>().join(", ");
|
||||
let unknown_message = format!("unknown arm key {{}} for union {enum_name}; declared keys: {declared}");
|
||||
let arity_message = format!("union {enum_name} is not a [key, slots] pair");
|
||||
let non_integer_message = format!("union {enum_name} has a non-integer arm key; declared keys: {declared}");
|
||||
let out_of_range_message = format!("union {enum_name} has an arm key outside Int32; declared keys: {declared}");
|
||||
let payload_checks = arms.iter().filter_map(|arm| arm.payload.as_ref()).map(|ty| {
|
||||
quote! { assert_slot_record::<#ty>(); }
|
||||
});
|
||||
|
||||
let serialize_arms = arms.iter().map(|arm| {
|
||||
let variant = &arm.ident;
|
||||
|
|
@ -441,6 +474,14 @@ fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
|
|||
});
|
||||
|
||||
Ok(quote! {
|
||||
// An arm is a slots array: a newtype over anything else is refused here, by the
|
||||
// trait the CultSlots and DatabaseEntry derives implement.
|
||||
const _: fn() = || {
|
||||
#[allow(dead_code)]
|
||||
fn assert_slot_record<T: ::cultcache_rs::SlotRecord>() {}
|
||||
#(#payload_checks)*
|
||||
};
|
||||
|
||||
impl ::serde::Serialize for #ident {
|
||||
fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
|
|
@ -460,6 +501,92 @@ fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
|
|||
where
|
||||
D: ::serde::Deserializer<'de>,
|
||||
{
|
||||
// The reference classifies the key before the arm: an integer in Int32, an
|
||||
// integer outside it, or not an integer. No input value is kept or echoed.
|
||||
enum ArmKey {
|
||||
Int(i64),
|
||||
OutOfRange,
|
||||
NonInteger,
|
||||
}
|
||||
struct ArmKeyProbe(ArmKey);
|
||||
struct ArmKeyVisitor;
|
||||
|
||||
impl<'de> ::serde::de::Visitor<'de> for ArmKeyVisitor {
|
||||
type Value = ArmKeyProbe;
|
||||
|
||||
fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
|
||||
formatter.write_str("an integer arm key")
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, value: i64) -> ::std::result::Result<Self::Value, E> {
|
||||
Ok(ArmKeyProbe(ArmKey::Int(value)))
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, value: u64) -> ::std::result::Result<Self::Value, E> {
|
||||
Ok(ArmKeyProbe(match i64::try_from(value) {
|
||||
Ok(value) => ArmKey::Int(value),
|
||||
Err(_) => ArmKey::OutOfRange,
|
||||
}))
|
||||
}
|
||||
|
||||
fn visit_bool<E>(self, _: bool) -> ::std::result::Result<Self::Value, E> {
|
||||
Ok(ArmKeyProbe(ArmKey::NonInteger))
|
||||
}
|
||||
|
||||
fn visit_f64<E>(self, _: f64) -> ::std::result::Result<Self::Value, E> {
|
||||
Ok(ArmKeyProbe(ArmKey::NonInteger))
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, _: &str) -> ::std::result::Result<Self::Value, E> {
|
||||
Ok(ArmKeyProbe(ArmKey::NonInteger))
|
||||
}
|
||||
|
||||
fn visit_bytes<E>(self, _: &[u8]) -> ::std::result::Result<Self::Value, E> {
|
||||
Ok(ArmKeyProbe(ArmKey::NonInteger))
|
||||
}
|
||||
|
||||
fn visit_unit<E>(self) -> ::std::result::Result<Self::Value, E> {
|
||||
Ok(ArmKeyProbe(ArmKey::NonInteger))
|
||||
}
|
||||
|
||||
fn visit_none<E>(self) -> ::std::result::Result<Self::Value, E> {
|
||||
Ok(ArmKeyProbe(ArmKey::NonInteger))
|
||||
}
|
||||
|
||||
fn visit_some<D>(self, deserializer: D) -> ::std::result::Result<Self::Value, D::Error>
|
||||
where
|
||||
D: ::serde::Deserializer<'de>,
|
||||
{
|
||||
<::serde::de::IgnoredAny as ::serde::Deserialize>::deserialize(deserializer)?;
|
||||
Ok(ArmKeyProbe(ArmKey::NonInteger))
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> ::std::result::Result<Self::Value, A::Error>
|
||||
where
|
||||
A: ::serde::de::SeqAccess<'de>,
|
||||
{
|
||||
while seq.next_element::<::serde::de::IgnoredAny>()?.is_some() {}
|
||||
Ok(ArmKeyProbe(ArmKey::NonInteger))
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> ::std::result::Result<Self::Value, A::Error>
|
||||
where
|
||||
A: ::serde::de::MapAccess<'de>,
|
||||
{
|
||||
while map.next_entry::<::serde::de::IgnoredAny, ::serde::de::IgnoredAny>()?.is_some() {}
|
||||
Ok(ArmKeyProbe(ArmKey::NonInteger))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> ::serde::Deserialize<'de> for ArmKeyProbe {
|
||||
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: ::serde::Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(ArmKeyVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct CultUnionVisitor;
|
||||
|
||||
impl<'de> ::serde::de::Visitor<'de> for CultUnionVisitor {
|
||||
|
|
@ -473,10 +600,28 @@ fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
|
|||
where
|
||||
A: ::serde::de::SeqAccess<'de>,
|
||||
{
|
||||
let key = seq
|
||||
.next_element::<u64>()
|
||||
if seq.size_hint().is_some_and(|len| len != 2) {
|
||||
return ::std::result::Result::Err(::serde::de::Error::custom(#arity_message));
|
||||
}
|
||||
let key = match seq
|
||||
.next_element::<ArmKeyProbe>()
|
||||
.map_err(|_| ::serde::de::Error::custom(#arity_message))?
|
||||
.ok_or_else(|| ::serde::de::Error::custom(#arity_message))?;
|
||||
.ok_or_else(|| ::serde::de::Error::custom(#arity_message))?
|
||||
.0
|
||||
{
|
||||
ArmKey::Int(wide) => match i32::try_from(wide) {
|
||||
Ok(key) => key,
|
||||
Err(_) => {
|
||||
return ::std::result::Result::Err(::serde::de::Error::custom(#out_of_range_message));
|
||||
}
|
||||
},
|
||||
ArmKey::OutOfRange => {
|
||||
return ::std::result::Result::Err(::serde::de::Error::custom(#out_of_range_message));
|
||||
}
|
||||
ArmKey::NonInteger => {
|
||||
return ::std::result::Result::Err(::serde::de::Error::custom(#non_integer_message));
|
||||
}
|
||||
};
|
||||
let value = match key {
|
||||
#(#deserialize_arms)*
|
||||
other => {
|
||||
|
|
@ -497,3 +642,100 @@ fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn union_error(source: &str) -> String {
|
||||
let input: DeriveInput = syn::parse_str(source).expect("the fixture parses");
|
||||
union_codec(input).expect_err("the fixture is refused").to_string()
|
||||
}
|
||||
|
||||
fn slots_error(source: &str) -> String {
|
||||
let input: DeriveInput = syn::parse_str(source).expect("the fixture parses");
|
||||
let Err(error) = named_slots(input.data, &input.ident, "CultSlots") else {
|
||||
panic!("the fixture is refused");
|
||||
};
|
||||
error.to_string()
|
||||
}
|
||||
|
||||
fn union_ok(source: &str) {
|
||||
let input: DeriveInput = syn::parse_str(source).expect("the fixture parses");
|
||||
union_codec(input).expect("the fixture is accepted");
|
||||
}
|
||||
|
||||
// Keys other than 0 throughout, so a default of 0 cannot stand in for a missing key.
|
||||
#[test]
|
||||
fn a_variant_without_a_key_is_refused_for_that_reason() {
|
||||
let message = union_error("enum E { #[cultcache(key = 4)] A(Arm), B }");
|
||||
assert!(message.contains("require #[cultcache(key = N)]"), "{message}");
|
||||
let message = union_error("enum E { B, #[cultcache(key = 4)] A(Arm) }");
|
||||
assert!(message.contains("require #[cultcache(key = N)]"), "{message}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_variants_with_one_key_are_refused_for_that_reason() {
|
||||
let message = union_error("enum E { #[cultcache(key = 4)] A(Arm), #[cultcache(key = 4)] B }");
|
||||
assert!(message.contains("duplicate CultUnion key 4"), "{message}");
|
||||
let message = union_error("enum E { #[cultcache(key = 0x4)] A(Arm), #[cultcache(key = 4)] B }");
|
||||
assert!(message.contains("duplicate CultUnion key 4"), "{message}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_repeated_key_attribute_is_refused_on_a_variant_and_on_a_field() {
|
||||
for source in [
|
||||
"enum E { #[cultcache(key = 7)] #[cultcache(key = 0)] A(Arm) }",
|
||||
"enum E { #[cultcache(key = 7, key = 0)] A(Arm) }",
|
||||
] {
|
||||
let message = union_error(source);
|
||||
assert!(message.contains("repeated `key`"), "{source}: {message}");
|
||||
}
|
||||
for source in [
|
||||
"struct S { #[cultcache(key = 7)] #[cultcache(key = 0)] a: String }",
|
||||
"struct S { #[cultcache(key = 7, key = 0)] a: String }",
|
||||
] {
|
||||
let message = slots_error(source);
|
||||
assert!(message.contains("repeated `key`"), "{source}: {message}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_duplicate_slot_is_refused_for_that_reason() {
|
||||
let message = slots_error("struct S { #[cultcache(key = 3)] a: String, #[cultcache(key = 3)] b: String }");
|
||||
assert!(message.contains("duplicate CultSlots slot 3"), "{message}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_field_without_a_key_is_refused_for_that_reason() {
|
||||
let message = slots_error("struct S { #[cultcache(key = 3)] a: String, b: String }");
|
||||
assert!(message.contains("fields require #[cultcache(key = N)]"), "{message}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn struct_multi_field_and_generic_variants_are_refused_for_their_reasons() {
|
||||
let message = union_error("enum E { #[cultcache(key = 4)] A { id: String } }");
|
||||
assert!(message.contains("use a newtype over a #[derive(CultSlots)] struct"), "{message}");
|
||||
let message = union_error("enum E { #[cultcache(key = 4)] A(Arm, Arm) }");
|
||||
assert!(message.contains("use a newtype over a #[derive(CultSlots)] struct"), "{message}");
|
||||
let message = union_error("enum E<T> { #[cultcache(key = 4)] A(Arm), #[cultcache(key = 5)] B(T) }");
|
||||
assert!(message.contains("does not support generics"), "{message}");
|
||||
}
|
||||
|
||||
// The arm key domain is the reference's Int32: negatives declare, Int32::MAX declares,
|
||||
// one past it is a compile error.
|
||||
#[test]
|
||||
fn the_arm_key_domain_is_int32() {
|
||||
union_ok("enum E { #[cultcache(key = -1)] A(Arm), #[cultcache(key = 2147483647)] B, #[cultcache(key = -2147483648)] C }");
|
||||
for source in [
|
||||
"enum E { #[cultcache(key = 2147483648)] A(Arm) }",
|
||||
"enum E { #[cultcache(key = -2147483649)] A(Arm) }",
|
||||
"enum E { #[cultcache(key = 4294967296)] A(Arm) }",
|
||||
] {
|
||||
let message = union_error(source);
|
||||
assert!(message.contains("outside Int32"), "{source}: {message}");
|
||||
}
|
||||
let message = union_error("enum E { #[cultcache(key = \"1\")] A(Arm) }");
|
||||
assert!(message.contains("integer literal"), "{message}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,32 @@
|
|||
//! The derives refuse a malformed union at compile time. Each block below is a valid
|
||||
//! union with one property changed.
|
||||
//! union with one property changed; keys are never 0, so a refusal cannot be a default
|
||||
//! key colliding with a declared 0. The derive crate's unit tests assert the message of
|
||||
//! every refusal the derive itself makes; a `compile_fail` block shows only that it fails.
|
||||
//!
|
||||
//! ```
|
||||
//! use cultcache_rs::{CultSlots, CultUnion};
|
||||
//! #[derive(CultSlots)]
|
||||
//! struct Arm { #[cultcache(key = 0)] id: String }
|
||||
//! struct Arm { #[cultcache(key = 3)] id: String }
|
||||
//! #[derive(CultUnion)]
|
||||
//! enum Ok_ { #[cultcache(key = 0)] A(Arm), #[cultcache(key = 1)] B }
|
||||
//! enum Fine { #[cultcache(key = -1)] A(Arm), #[cultcache(key = 2147483647)] B, #[cultcache(key = 4)] C }
|
||||
//! ```
|
||||
//!
|
||||
//! A variant without a key:
|
||||
//! A variant without a key, first and last:
|
||||
//!
|
||||
//! ```compile_fail
|
||||
//! use cultcache_rs::{CultSlots, CultUnion};
|
||||
//! #[derive(CultSlots)]
|
||||
//! struct Arm { #[cultcache(key = 0)] id: String }
|
||||
//! struct Arm { #[cultcache(key = 3)] id: String }
|
||||
//! #[derive(CultUnion)]
|
||||
//! enum Bad { #[cultcache(key = 0)] A(Arm), B }
|
||||
//! enum Bad { #[cultcache(key = 4)] A(Arm), B }
|
||||
//! ```
|
||||
//!
|
||||
//! ```compile_fail
|
||||
//! use cultcache_rs::{CultSlots, CultUnion};
|
||||
//! #[derive(CultSlots)]
|
||||
//! struct Arm { #[cultcache(key = 3)] id: String }
|
||||
//! #[derive(CultUnion)]
|
||||
//! enum Bad { B, #[cultcache(key = 4)] A(Arm) }
|
||||
//! ```
|
||||
//!
|
||||
//! Two variants with one key:
|
||||
|
|
@ -24,9 +34,25 @@
|
|||
//! ```compile_fail
|
||||
//! use cultcache_rs::{CultSlots, CultUnion};
|
||||
//! #[derive(CultSlots)]
|
||||
//! struct Arm { #[cultcache(key = 0)] id: String }
|
||||
//! struct Arm { #[cultcache(key = 3)] id: String }
|
||||
//! #[derive(CultUnion)]
|
||||
//! enum Bad { #[cultcache(key = 0)] A(Arm), #[cultcache(key = 0)] B }
|
||||
//! enum Bad { #[cultcache(key = 4)] A(Arm), #[cultcache(key = 4)] B }
|
||||
//! ```
|
||||
//!
|
||||
//! A key attribute given twice:
|
||||
//!
|
||||
//! ```compile_fail
|
||||
//! use cultcache_rs::CultUnion;
|
||||
//! #[derive(CultUnion)]
|
||||
//! enum Bad { #[cultcache(key = 4)] #[cultcache(key = 5)] A }
|
||||
//! ```
|
||||
//!
|
||||
//! A key outside Int32:
|
||||
//!
|
||||
//! ```compile_fail
|
||||
//! use cultcache_rs::CultUnion;
|
||||
//! #[derive(CultUnion)]
|
||||
//! enum Bad { #[cultcache(key = 2147483648)] A }
|
||||
//! ```
|
||||
//!
|
||||
//! A struct variant:
|
||||
|
|
@ -34,7 +60,7 @@
|
|||
//! ```compile_fail
|
||||
//! use cultcache_rs::CultUnion;
|
||||
//! #[derive(CultUnion)]
|
||||
//! enum Bad { #[cultcache(key = 0)] A { id: String }, #[cultcache(key = 1)] B }
|
||||
//! enum Bad { #[cultcache(key = 4)] A { id: String }, #[cultcache(key = 5)] B }
|
||||
//! ```
|
||||
//!
|
||||
//! A multi-field tuple variant:
|
||||
|
|
@ -42,9 +68,9 @@
|
|||
//! ```compile_fail
|
||||
//! use cultcache_rs::{CultSlots, CultUnion};
|
||||
//! #[derive(CultSlots)]
|
||||
//! struct Arm { #[cultcache(key = 0)] id: String }
|
||||
//! struct Arm { #[cultcache(key = 3)] id: String }
|
||||
//! #[derive(CultUnion)]
|
||||
//! enum Bad { #[cultcache(key = 0)] A(Arm, Arm), #[cultcache(key = 1)] B }
|
||||
//! enum Bad { #[cultcache(key = 4)] A(Arm, Arm), #[cultcache(key = 5)] B }
|
||||
//! ```
|
||||
//!
|
||||
//! A generic enum:
|
||||
|
|
@ -52,23 +78,51 @@
|
|||
//! ```compile_fail
|
||||
//! use cultcache_rs::{CultSlots, CultUnion};
|
||||
//! #[derive(CultSlots)]
|
||||
//! struct Arm { #[cultcache(key = 0)] id: String }
|
||||
//! struct Arm { #[cultcache(key = 3)] id: String }
|
||||
//! #[derive(CultUnion)]
|
||||
//! enum Bad<T> { #[cultcache(key = 0)] A(Arm), #[cultcache(key = 1)] B(std::marker::PhantomData<T>) }
|
||||
//! enum Bad<T> { #[cultcache(key = 4)] A(Arm), #[cultcache(key = 5)] B(std::marker::PhantomData<T>) }
|
||||
//! ```
|
||||
//!
|
||||
//! A CultSlots field without a key, and a generic CultSlots struct:
|
||||
//! An arm that is not a slots record (rustc's `SlotRecord` bound, not the derive, refuses it):
|
||||
//!
|
||||
//! ```compile_fail
|
||||
//! use cultcache_rs::CultUnion;
|
||||
//! #[derive(CultUnion)]
|
||||
//! enum Bad { #[cultcache(key = 4)] A(String) }
|
||||
//! ```
|
||||
//!
|
||||
//! ```compile_fail
|
||||
//! use cultcache_rs::{CultSlots, CultUnion};
|
||||
//! #[derive(CultSlots)]
|
||||
//! struct Arm { #[cultcache(key = 3)] id: String }
|
||||
//! #[derive(CultUnion)]
|
||||
//! enum Bad { #[cultcache(key = 4)] A(Option<Arm>) }
|
||||
//! ```
|
||||
//!
|
||||
//! A CultSlots field without a key, a repeated slot, a repeated key attribute, and a generic struct:
|
||||
//!
|
||||
//! ```compile_fail
|
||||
//! use cultcache_rs::CultSlots;
|
||||
//! #[derive(CultSlots)]
|
||||
//! struct Arm { #[cultcache(key = 0)] id: String, name: String }
|
||||
//! struct Arm { #[cultcache(key = 3)] id: String, name: String }
|
||||
//! ```
|
||||
//!
|
||||
//! ```compile_fail
|
||||
//! use cultcache_rs::CultSlots;
|
||||
//! #[derive(CultSlots)]
|
||||
//! struct Arm<T> { #[cultcache(key = 0)] id: T }
|
||||
//! struct Arm { #[cultcache(key = 3)] id: String, #[cultcache(key = 3)] name: String }
|
||||
//! ```
|
||||
//!
|
||||
//! ```compile_fail
|
||||
//! use cultcache_rs::CultSlots;
|
||||
//! #[derive(CultSlots)]
|
||||
//! struct Arm { #[cultcache(key = 3)] #[cultcache(key = 4)] id: String }
|
||||
//! ```
|
||||
//!
|
||||
//! ```compile_fail
|
||||
//! use cultcache_rs::CultSlots;
|
||||
//! #[derive(CultSlots)]
|
||||
//! struct Arm<T> { #[cultcache(key = 3)] id: T }
|
||||
//! ```
|
||||
|
||||
use anyhow::Context;
|
||||
|
|
@ -102,6 +156,15 @@ pub trait DatabaseEntry: Serialize + DeserializeOwned + Clone + Send + 'static {
|
|||
const SCHEMA_NAME: &'static str = "DatabaseEntry";
|
||||
}
|
||||
|
||||
/// A type written as a positional slots array: what the `CultSlots` and `DatabaseEntry`
|
||||
/// derives implement, and what a `CultUnion` arm must carry.
|
||||
#[diagnostic::on_unimplemented(
|
||||
message = "`{Self}` is not a slots record",
|
||||
label = "a CultUnion arm is a newtype over a #[derive(CultSlots)] struct",
|
||||
note = "a String, an Option or any other type is not written as an array of slots"
|
||||
)]
|
||||
pub trait SlotRecord {}
|
||||
|
||||
pub trait CultCacheRegistry {
|
||||
fn register_entries(&self, cache: &mut CultCache) -> Result<()>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -293,6 +293,132 @@ fn union_store_reads() {
|
|||
assert_eq!(cache.get::<UnionSurface>("surface").unwrap(), Some(constructed()));
|
||||
}
|
||||
|
||||
// The arm key domain is the reference's Int32. Expected bytes are what MessagePack-CSharp writes
|
||||
// for the same keys (WriteInt32: fixneg, uint32, int32); the refusal classes are CultUnionGuard's.
|
||||
mod arm_keys {
|
||||
use cultcache_rs::CultSlots;
|
||||
use cultcache_rs::CultUnion;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
struct Count {
|
||||
#[cultcache(key = 0)]
|
||||
name: String,
|
||||
#[cultcache(key = 1)]
|
||||
count: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
enum Keyed {
|
||||
#[cultcache(key = -1)]
|
||||
Negative(Count),
|
||||
#[cultcache(key = 2147483647)]
|
||||
Max(Count),
|
||||
#[cultcache(key = -2147483648)]
|
||||
Min,
|
||||
#[cultcache(key = 7)]
|
||||
Plain(Count),
|
||||
}
|
||||
|
||||
fn count() -> Count {
|
||||
Count { name: "n".into(), count: 1 }
|
||||
}
|
||||
|
||||
fn message(bytes: &[u8]) -> String {
|
||||
rmp_serde::from_slice::<Keyed>(bytes).unwrap_err().to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_and_int32_max_keys_are_declared_and_round_trip() {
|
||||
let cases: [(Keyed, &[u8]); 4] = [
|
||||
(Keyed::Negative(count()), &[0x92, 0xff, 0x92, 0xa1, b'n', 0x01]),
|
||||
(Keyed::Max(count()), &[0x92, 0xce, 0x7f, 0xff, 0xff, 0xff, 0x92, 0xa1, b'n', 0x01]),
|
||||
(Keyed::Min, &[0x92, 0xd2, 0x80, 0x00, 0x00, 0x00, 0x90]),
|
||||
(Keyed::Plain(count()), &[0x92, 0x07, 0x92, 0xa1, b'n', 0x01]),
|
||||
];
|
||||
for (value, bytes) in cases {
|
||||
assert_eq!(rmp_serde::to_vec(&value).unwrap(), bytes, "{value:?}");
|
||||
assert_eq!(rmp_serde::from_slice::<Keyed>(bytes).unwrap(), value);
|
||||
}
|
||||
}
|
||||
|
||||
// Every integer width of a negative key reads as the same arm.
|
||||
#[test]
|
||||
fn a_negative_key_reads_in_every_integer_width() {
|
||||
for key in [&[0xff][..], &[0xd0, 0xff], &[0xd1, 0xff, 0xff], &[0xd2, 0xff, 0xff, 0xff, 0xff], &[0xd3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]] {
|
||||
let mut bytes = vec![0x92];
|
||||
bytes.extend_from_slice(key);
|
||||
bytes.extend_from_slice(&[0x92, 0xa1, b'n', 0x01]);
|
||||
assert_eq!(rmp_serde::from_slice::<Keyed>(&bytes).unwrap(), Keyed::Negative(count()), "{key:02x?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn pair(key: &[u8]) -> Vec<u8> {
|
||||
let mut bytes = vec![0x92];
|
||||
bytes.extend_from_slice(key);
|
||||
bytes.extend_from_slice(&[0x92, 0xa1, b'C', 0xa1, b'A']);
|
||||
bytes
|
||||
}
|
||||
|
||||
// A negative key the union does not declare is an unknown arm naming the key, as in the reference.
|
||||
#[test]
|
||||
fn a_negative_undeclared_key_is_an_unknown_arm_naming_the_key() {
|
||||
for (key, shown) in [(&[0xe0][..], "-32"), (&[0xd0, 0xdf], "-33"), (&[0xd1, 0x80, 0x00], "-32768"), (&[0xd2, 0x80, 0x00, 0x00, 0x01], "-2147483647")] {
|
||||
let text = message(&pair(key));
|
||||
assert!(text.contains(&format!("unknown arm key {shown};")), "{text}");
|
||||
assert!(text.contains("declared keys: -2147483648, -1, 7, 2147483647"), "{text}");
|
||||
assert!(!text.contains("CA"), "{text}");
|
||||
}
|
||||
}
|
||||
|
||||
// Outside Int32 is its own class, naming neither the value nor the payload.
|
||||
#[test]
|
||||
fn keys_outside_int32_are_their_own_class_and_never_echoed() {
|
||||
let wide: [&[u8]; 6] = [
|
||||
&[0xce, 0x80, 0x00, 0x00, 0x00],
|
||||
&[0xcf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00],
|
||||
&[0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
|
||||
&[0xd3, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xff, 0xff, 0xff],
|
||||
&[0xd3, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
&[0xcf, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
|
||||
];
|
||||
for key in wide {
|
||||
let text = message(&pair(key));
|
||||
assert!(text.contains("an arm key outside Int32"), "{key:02x?}: {text}");
|
||||
assert!(text.contains("Keyed") && !text.contains("CA"), "{text}");
|
||||
for value in ["4294967296", "9223372036854775807", "9223372036854775808", "18446744073709551615"] {
|
||||
assert!(!text.contains(value), "{text}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_key_that_is_not_an_integer_is_its_own_class_and_never_echoed() {
|
||||
let keys: [&[u8]; 7] = [
|
||||
&[0xc0],
|
||||
&[0xc3],
|
||||
&[0xcb, 0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
&[0xa6, b'C', b'A', b'N', b'A', b'R', b'Y'],
|
||||
&[0xc4, 0x02, b'C', b'A'],
|
||||
&[0x91, 0xa6, b'C', b'A', b'N', b'A', b'R', b'Y'],
|
||||
&[0x81, 0xa6, b'C', b'A', b'N', b'A', b'R', b'Y', 0x01],
|
||||
];
|
||||
for key in keys {
|
||||
let text = message(&pair(key));
|
||||
assert!(text.contains("a non-integer arm key") && text.contains("Keyed"), "{key:02x?}: {text}");
|
||||
assert!(!text.contains("CANARY") && !text.contains("CA"), "{text}");
|
||||
}
|
||||
}
|
||||
|
||||
// Anything that is not a two-element array is the arity class, whatever its first element is.
|
||||
#[test]
|
||||
fn a_pair_of_the_wrong_length_is_refused_before_its_key_is_read() {
|
||||
for bytes in [&[0x91, 0xa1, b'x'][..], &[0x93, 0x07, 0x90, 0x00], &[0x90]] {
|
||||
let text = message(bytes);
|
||||
assert!(text.contains("is not a [key, slots] pair"), "{bytes:02x?}: {text}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod serde_enum_bytes {
|
||||
use cultcache_rs::DatabaseEntry;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue