cultcache-rs: CultSlots and CultUnion derives writing the reference [key, armSlots] shape

One slot generator now serves DatabaseEntry and CultSlots. CultUnion writes
[key, slots] with unit arms as [key, []] and refuses unknown keys naming the
enum and declared keys. Tests hold the runtime to tests/vectors/union-parity.

Not yet built or run: this commit may not build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-11 03:50:30 +02:00
commit 2d840d5ffb
4 changed files with 700 additions and 33 deletions

View file

@ -184,6 +184,49 @@ without pretending Rust has runtime reflection hiding under the floorboards.
- `load_soa`
- `SingleFileMessagePackBackingStore`
## Unions
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
quoting the payload. A null union is `Option<Enum>`. Plain serde enums are untouched and keep their bytes.
```rust
use cultcache_rs::{CultSlots, CultUnion};
#[derive(Clone, Debug, PartialEq, CultUnion)]
enum Node {
#[cultcache(key = 0)]
Box(BoxArm),
#[cultcache(key = 1)]
Text(TextArm),
#[cultcache(key = 5)]
Spacer, // a unit arm: [5, []]
}
#[derive(Clone, Debug, PartialEq, CultSlots)]
struct BoxArm {
#[cultcache(key = 0)]
id: String,
#[cultcache(key = 1)]
children: Vec<Node>, // recursive
}
#[derive(Clone, Debug, PartialEq, CultSlots)]
struct TextArm {
#[cultcache(key = 0)]
id: String,
#[cultcache(key = 1)]
content: String,
}
let tree = Node::Box(BoxArm { id: "root".into(), children: vec![Node::Spacer] });
let bytes = rmp_serde::to_vec(&tree)?;
assert_eq!(rmp_serde::from_slice::<Node>(&bytes)?, tree);
# Ok::<(), Box<dyn std::error::Error>>(())
```
## Structure Of Arrays
The C# CultCache hot path exposes CPU-local SoA scans as:

View file

@ -47,31 +47,79 @@ pub fn derive_database_entry(input: TokenStream) -> TokenStream {
};
let schema_name = schema_name.unwrap_or_else(|| LitStr::new(&ident.to_string(), ident.span()));
let fields = match input.data {
let slots = match named_slots(input.data, &ident, "DatabaseEntry") {
Ok(slots) => slots,
Err(error) => return error.to_compile_error().into(),
};
let codec = slot_codec(&ident, &slots);
quote! {
impl ::cultcache_rs::DatabaseEntry for #ident {
const TYPE: &'static str = #entry_type;
const SCHEMA_NAME: &'static str = #schema_name;
}
#codec
}
.into()
}
/// A named struct whose fields carry `#[cultcache(key = N)]`: the same positional
/// array a `DatabaseEntry` is, without the entry consts. The payload of a union arm,
/// or any nested record that crosses runtimes.
#[proc_macro_derive(CultSlots, attributes(cultcache))]
pub fn derive_cult_slots(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let ident = input.ident.clone();
if !input.generics.params.is_empty() {
return syn::Error::new_spanned(ident, "CultSlots derive does not support generics")
.to_compile_error()
.into();
}
match named_slots(input.data, &ident, "CultSlots") {
Ok(slots) => slot_codec(&ident, &slots).into(),
Err(error) => error.to_compile_error().into(),
}
}
/// An enum written as `[key, armSlots]`. Every variant carries `#[cultcache(key = N)]`
/// and is a unit (`[N, []]`) or a newtype over a type that serializes as an array.
/// A key the enum does not declare is a decode error; there is no catch-all arm.
#[proc_macro_derive(CultUnion, attributes(cultcache))]
pub fn derive_cult_union(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match union_codec(input) {
Ok(tokens) => tokens.into(),
Err(error) => error.to_compile_error().into(),
}
}
fn named_slots(data: Data, ident: &syn::Ident, owner: &str) -> syn::Result<Vec<CultCacheSlot>> {
let fields = match data {
Data::Struct(data) => match data.fields {
Fields::Named(fields) => fields.named,
_ => {
return syn::Error::new_spanned(
return Err(syn::Error::new_spanned(
ident,
"DatabaseEntry derive requires a named struct",
)
.to_compile_error()
.into();
format!("{owner} derive requires a named struct"),
));
}
},
_ => {
return syn::Error::new_spanned(ident, "DatabaseEntry derive requires a struct")
.to_compile_error()
.into();
return Err(syn::Error::new_spanned(
ident,
format!("{owner} derive requires a struct"),
));
}
};
let mut slots: Vec<CultCacheSlot> = Vec::new();
for field in fields {
let Some(field_ident) = field.ident else {
return syn::Error::new_spanned(field.ty, "DatabaseEntry derive requires named fields")
.to_compile_error()
.into();
return Err(syn::Error::new_spanned(
field.ty,
format!("{owner} derive requires named fields"),
));
};
let mut key: Option<usize> = None;
let mut default = false;
@ -81,7 +129,7 @@ pub fn derive_database_entry(input: TokenStream) -> TokenStream {
.iter()
.filter(|attr| attr.path().is_ident("cultcache"))
{
let parse_result = attribute.parse_nested_meta(|meta| {
attribute.parse_nested_meta(|meta| {
if meta.path.is_ident("key") {
let value: LitInt = meta.value()?.parse()?;
key = Some(value.base10_parse()?);
@ -95,26 +143,19 @@ pub fn derive_database_entry(input: TokenStream) -> TokenStream {
} else {
Err(meta.error("unsupported field cultcache attribute"))
}
});
if let Err(error) = parse_result {
return error.to_compile_error().into();
}
})?;
}
let Some(key) = key else {
return syn::Error::new_spanned(
return Err(syn::Error::new_spanned(
field_ident,
"DatabaseEntry fields require #[cultcache(key = N)]",
)
.to_compile_error()
.into();
format!("{owner} fields require #[cultcache(key = N)]"),
));
};
if slots.iter().any(|slot| slot.key == key) {
return syn::Error::new_spanned(
return Err(syn::Error::new_spanned(
field_ident,
format!("duplicate DatabaseEntry slot {key}"),
)
.to_compile_error()
.into();
format!("duplicate {owner} slot {key}"),
));
}
slots.push(CultCacheSlot {
key,
@ -125,6 +166,12 @@ pub fn derive_database_entry(input: TokenStream) -> TokenStream {
});
}
slots.sort_by_key(|slot| slot.key);
Ok(slots)
}
/// The one slot generator: Serialize as a positional array with nil gaps, Deserialize
/// by slot with the `default` and `bytes` attributes.
fn slot_codec(ident: &syn::Ident, slots: &[CultCacheSlot]) -> proc_macro2::TokenStream {
let max_slot = slots.iter().map(|slot| slot.key).max().unwrap_or(0);
let serialize_elements = (0..=max_slot).map(|slot| {
@ -211,11 +258,6 @@ pub fn derive_database_entry(input: TokenStream) -> TokenStream {
});
quote! {
impl ::cultcache_rs::DatabaseEntry for #ident {
const TYPE: &'static str = #entry_type;
const SCHEMA_NAME: &'static str = #schema_name;
}
impl ::serde::Serialize for #ident {
fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
where
@ -266,7 +308,6 @@ pub fn derive_database_entry(input: TokenStream) -> TokenStream {
}
}
}
.into()
}
struct CultCacheSlot {
@ -276,3 +317,183 @@ struct CultCacheSlot {
default: bool,
bytes: bool,
}
fn union_codec(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let ident = input.ident.clone();
if !input.generics.params.is_empty() {
return Err(syn::Error::new_spanned(
ident,
"CultUnion derive does not support generics",
));
}
let Data::Enum(data) = input.data else {
return Err(syn::Error::new_spanned(ident, "CultUnion derive requires an enum"));
};
if data.variants.is_empty() {
return Err(syn::Error::new_spanned(ident, "CultUnion derive requires at least one variant"));
}
struct Arm {
key: u64,
ident: syn::Ident,
payload: Option<Type>,
}
let mut arms: Vec<Arm> = Vec::new();
for variant in data.variants {
let mut key: Option<u64> = None;
for attribute in variant
.attrs
.iter()
.filter(|attr| attr.path().is_ident("cultcache"))
{
attribute.parse_nested_meta(|meta| {
if meta.path.is_ident("key") {
let value: LitInt = meta.value()?.parse()?;
key = Some(value.base10_parse()?);
Ok(())
} else {
Err(meta.error("unsupported variant cultcache attribute"))
}
})?;
}
let Some(key) = key else {
return Err(syn::Error::new_spanned(
&variant.ident,
"CultUnion variants require #[cultcache(key = N)]",
));
};
if arms.iter().any(|arm| arm.key == key) {
return Err(syn::Error::new_spanned(
&variant.ident,
format!("duplicate CultUnion key {key}"),
));
}
let payload = match variant.fields {
Fields::Unit => None,
Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
Some(fields.unnamed.into_iter().next().expect("one field").ty)
}
_ => {
return Err(syn::Error::new_spanned(
&variant.ident,
"a CultUnion variant is a unit or a newtype: use a newtype over a #[derive(CultSlots)] struct",
));
}
};
arms.push(Arm {
key,
ident: variant.ident,
payload,
});
}
let enum_name = ident.to_string();
let declared = arms
.iter()
.map(|arm| arm.key.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 serialize_arms = arms.iter().map(|arm| {
let variant = &arm.ident;
let key = arm.key;
if arm.payload.is_some() {
quote! {
Self::#variant(arm) => {
tuple.serialize_element(&#key)?;
tuple.serialize_element(arm)?;
}
}
} else {
quote! {
Self::#variant => {
tuple.serialize_element(&#key)?;
tuple.serialize_element(&[(); 0])?;
}
}
}
});
let deserialize_arms = arms.iter().map(|arm| {
let variant = &arm.ident;
let key = arm.key;
if let Some(ty) = &arm.payload {
quote! {
#key => {
let arm = seq
.next_element::<#ty>()?
.ok_or_else(|| ::serde::de::Error::custom(#arity_message))?;
#ident::#variant(arm)
}
}
} else {
quote! {
#key => {
seq.next_element::<[(); 0]>()
.map_err(|_| ::serde::de::Error::custom(#arity_message))?
.ok_or_else(|| ::serde::de::Error::custom(#arity_message))?;
#ident::#variant
}
}
}
});
Ok(quote! {
impl ::serde::Serialize for #ident {
fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
where
S: ::serde::Serializer,
{
use ::serde::ser::SerializeTuple;
let mut tuple = serializer.serialize_tuple(2)?;
match self {
#(#serialize_arms)*
}
tuple.end()
}
}
impl<'de> ::serde::Deserialize<'de> for #ident {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
struct CultUnionVisitor;
impl<'de> ::serde::de::Visitor<'de> for CultUnionVisitor {
type Value = #ident;
fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
formatter.write_str(concat!("a [key, slots] pair for union ", stringify!(#ident)))
}
fn visit_seq<A>(self, mut seq: A) -> ::std::result::Result<Self::Value, A::Error>
where
A: ::serde::de::SeqAccess<'de>,
{
let key = seq
.next_element::<u64>()
.map_err(|_| ::serde::de::Error::custom(#arity_message))?
.ok_or_else(|| ::serde::de::Error::custom(#arity_message))?;
let value = match key {
#(#deserialize_arms)*
other => {
return ::std::result::Result::Err(::serde::de::Error::custom(
format!(#unknown_message, other),
));
}
};
if seq.next_element::<::serde::de::IgnoredAny>()?.is_some() {
return ::std::result::Result::Err(::serde::de::Error::custom(#arity_message));
}
Ok(value)
}
}
deserializer.deserialize_tuple(2, CultUnionVisitor)
}
}
})
}

View file

@ -1,3 +1,62 @@
//! The derives refuse a malformed union at compile time. Each block below is a valid
//! union with one property changed.
//!
//! ```
//! use cultcache_rs::{CultSlots, CultUnion};
//! #[derive(CultSlots)]
//! struct Arm { #[cultcache(key = 0)] id: String }
//! #[derive(CultUnion)]
//! enum Ok_ { #[cultcache(key = 0)] A(Arm), #[cultcache(key = 1)] B }
//! ```
//!
//! A variant without a key:
//!
//! ```compile_fail
//! use cultcache_rs::{CultSlots, CultUnion};
//! #[derive(CultSlots)]
//! struct Arm { #[cultcache(key = 0)] id: String }
//! #[derive(CultUnion)]
//! enum Bad { #[cultcache(key = 0)] A(Arm), B }
//! ```
//!
//! Two variants with one key:
//!
//! ```compile_fail
//! use cultcache_rs::{CultSlots, CultUnion};
//! #[derive(CultSlots)]
//! struct Arm { #[cultcache(key = 0)] id: String }
//! #[derive(CultUnion)]
//! enum Bad { #[cultcache(key = 0)] A(Arm), #[cultcache(key = 0)] B }
//! ```
//!
//! A struct variant:
//!
//! ```compile_fail
//! use cultcache_rs::CultUnion;
//! #[derive(CultUnion)]
//! enum Bad { #[cultcache(key = 0)] A { id: String }, #[cultcache(key = 1)] B }
//! ```
//!
//! A multi-field tuple variant:
//!
//! ```compile_fail
//! use cultcache_rs::{CultSlots, CultUnion};
//! #[derive(CultSlots)]
//! struct Arm { #[cultcache(key = 0)] id: String }
//! #[derive(CultUnion)]
//! enum Bad { #[cultcache(key = 0)] A(Arm, Arm), #[cultcache(key = 1)] B }
//! ```
//!
//! A generic enum:
//!
//! ```compile_fail
//! use cultcache_rs::{CultSlots, CultUnion};
//! #[derive(CultSlots)]
//! struct Arm { #[cultcache(key = 0)] id: String }
//! #[derive(CultUnion)]
//! enum Bad<T> { #[cultcache(key = 0)] A(Arm), #[cultcache(key = 1)] B(std::marker::PhantomData<T>) }
//! ```
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
@ -20,6 +79,8 @@ use redb::{Database, ReadableDatabase, ReadableTable, TableDefinition};
extern crate self as cultcache_rs;
pub use cultcache_rs_derive::CultSlots;
pub use cultcache_rs_derive::CultUnion;
pub use cultcache_rs_derive::DatabaseEntry;
pub trait DatabaseEntry: Serialize + DeserializeOwned + Clone + Send + 'static {

View file

@ -0,0 +1,342 @@
//! The Rust runtime held to tests/vectors/union-parity, written by the C# reference.
//! Bytes and outcomes are read from the committed files; only the constructed value is spelled here.
use std::path::PathBuf;
use cultcache_rs::CultCache;
use cultcache_rs::CultSlots;
use cultcache_rs::CultUnion;
use cultcache_rs::DatabaseEntry;
use cultcache_rs::SingleFileMessagePackBackingStore;
#[derive(Clone, Debug, PartialEq, CultUnion)]
enum Node {
#[cultcache(key = 0)]
Box(BoxArm),
#[cultcache(key = 1)]
Text(TextArm),
#[cultcache(key = 2)]
Use(UseArm),
#[cultcache(key = 5)]
Image(ImageArm),
}
#[derive(Clone, Debug, PartialEq, CultSlots)]
struct BoxArm {
#[cultcache(key = 0)]
id: String,
#[cultcache(key = 1)]
children: Vec<Node>,
}
#[derive(Clone, Debug, PartialEq, CultSlots)]
struct TextArm {
#[cultcache(key = 0)]
id: String,
#[cultcache(key = 1)]
content: String,
}
#[derive(Clone, Debug, PartialEq, CultSlots)]
struct UseArm {
#[cultcache(key = 0)]
id: String,
#[cultcache(key = 1)]
composition: Composition,
#[cultcache(key = 2)]
slots: Vec<Option<Node>>,
}
// Slot 2 is retired: a gap written nil.
#[derive(Clone, Debug, PartialEq, CultSlots)]
struct ImageArm {
#[cultcache(key = 0)]
id: String,
#[cultcache(key = 1)]
uri: String,
#[cultcache(key = 3)]
alt: String,
}
#[derive(Clone, Debug, PartialEq, CultSlots)]
struct Composition {
#[cultcache(key = 0)]
name: String,
#[cultcache(key = 1)]
body: Option<Node>,
}
#[derive(Clone, Debug, PartialEq, CultUnion)]
enum Outcome {
#[cultcache(key = 0)]
Pending(PendingArm),
#[cultcache(key = 1)]
Completed(CompletedArm),
#[cultcache(key = 2)]
Denied,
}
#[derive(Clone, Debug, PartialEq, CultSlots)]
struct PendingArm {
#[cultcache(key = 0)]
message: String,
}
#[derive(Clone, Debug, PartialEq, CultSlots)]
struct CompletedArm {
#[cultcache(key = 0)]
source_version: i32,
}
#[derive(Clone, Debug, PartialEq, CultSlots)]
struct NamedArm {
#[cultcache(key = 0)]
name: String,
}
#[derive(Clone, Debug, PartialEq, CultSlots)]
struct UsesArm {
#[cultcache(key = 0)]
name: String,
#[cultcache(key = 1)]
uses: i32,
}
#[derive(Clone, Debug, PartialEq, CultSlots)]
struct CountArm {
#[cultcache(key = 0)]
name: String,
#[cultcache(key = 1)]
count: i32,
}
#[derive(Clone, Debug, PartialEq, CultUnion)]
enum Item {
#[cultcache(key = 0)]
Gem(NamedArm),
#[cultcache(key = 1)]
Tool(UsesArm),
#[cultcache(key = 2)]
Part(CountArm),
}
// The same arms as Item under its own keys: key 2 is unknown to it.
#[derive(Clone, Debug, PartialEq, CultUnion)]
enum CraftedItem {
#[cultcache(key = 0)]
Tool(UsesArm),
#[cultcache(key = 1)]
Part(CountArm),
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(type = "tests.union_surface", schema = "tests.union_surface.v1")]
struct UnionSurface {
#[cultcache(key = 0)]
id: String,
#[cultcache(key = 1)]
version: i32,
#[cultcache(key = 2)]
root: Option<Node>,
#[cultcache(key = 3)]
outcomes: Vec<Outcome>,
#[cultcache(key = 4)]
part: Option<Item>,
#[cultcache(key = 5)]
crafted: Option<CraftedItem>,
#[cultcache(key = 6)]
missing: Option<Node>,
}
fn text(id: &str, content: &str) -> Node {
Node::Text(TextArm { id: id.into(), content: content.into() })
}
fn image(id: &str, uri: &str, alt: &str) -> Node {
Node::Image(ImageArm { id: id.into(), uri: uri.into(), alt: alt.into() })
}
fn boxed(id: &str, children: Vec<Node>) -> Node {
Node::Box(BoxArm { id: id.into(), children })
}
fn constructed() -> UnionSurface {
let inner = boxed("inner", vec![text("deep", "x"), image("img", "uri://a", "alt")]);
UnionSurface {
id: "surface".into(),
version: 3,
root: Some(boxed(
"root",
vec![
text("t1", "hello"),
Node::Use(UseArm {
id: "u1".into(),
composition: Composition { name: "comp".into(), body: Some(inner) },
slots: vec![Some(text("s1", "slot")), None, Some(boxed("b2", vec![]))],
}),
image("i2", "uri://b", ""),
],
)),
outcomes: vec![
Outcome::Pending(PendingArm { message: "wait".into() }),
Outcome::Completed(CompletedArm { source_version: 7 }),
Outcome::Denied,
],
part: Some(Item::Tool(UsesArm { name: "hammer".into(), uses: 12 })),
crafted: Some(CraftedItem::Part(CountArm { name: "gear".into(), count: 4 })),
missing: None,
}
}
fn vector(name: &str) -> Vec<u8> {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/vectors/union-parity")
.join(name);
std::fs::read(&path).unwrap_or_else(|error| panic!("{}: {error}", path.display()))
}
fn decode(bytes: &[u8]) -> Result<UnionSurface, String> {
rmp_serde::from_slice(bytes).map_err(|error| error.to_string())
}
#[test]
fn union_surface_decodes_and_reencodes() {
let payload = vector("union-surface.msgpack");
let value = constructed();
assert_eq!(decode(&payload).unwrap(), value);
assert_eq!(rmp_serde::to_vec(&value).unwrap(), payload);
}
#[test]
fn a_unit_arm_is_a_key_and_an_empty_slots_array() {
assert_eq!(rmp_serde::to_vec(&Outcome::Denied).unwrap(), [0x92, 0x02, 0x90]);
assert_eq!(rmp_serde::from_slice::<Outcome>(&[0x92, 0x02, 0x90]).unwrap(), Outcome::Denied);
}
#[test]
fn union_refusal_vectors() {
let outcomes: Vec<(String, String, String, Option<u64>, String)> =
rmp_serde::from_slice(&vector("outcomes.msgpack")).unwrap();
let (mut refused, mut read_only) = (0, 0);
for (file, outcome, union, key, detail) in outcomes {
let result = decode(&vector(&file));
match outcome.as_str() {
"refused" => {
refused += 1;
let message = result.expect_err(&file);
assert!(message.contains(&union), "{file}: {message}");
if detail == "unknown-arm" {
let key = key.expect(&file);
assert!(message.contains(&format!("unknown arm key {key} ")), "{file}: {message}");
}
for canary in ["root", "hello", "uri://", "surface"] {
assert!(!message.contains(canary), "{file} echoed a value: {message}");
}
}
"read-only" => {
read_only += 1;
assert_eq!(result.expect(&file), constructed(), "{file}");
}
other => panic!("{file}: unknown outcome {other}"),
}
}
assert_eq!((refused, read_only), (5, 4));
}
#[test]
fn an_unknown_arm_lists_the_declared_keys_and_never_echoes_the_payload() {
// Shaped like a real arm, an array of strings, under an undeclared key.
let bytes = rmp_serde::to_vec(&(99u64, ("CANARY-7f3a", "CANARY-91bc"))).unwrap();
let message = rmp_serde::from_slice::<Node>(&bytes).unwrap_err().to_string();
assert!(message.contains("Node") && message.contains("99") && message.contains("0, 1, 2, 5"), "{message}");
assert!(!message.contains("CANARY"), "{message}");
}
#[test]
fn a_union_that_is_not_a_key_and_slots_pair_is_refused_naming_the_enum() {
let cases: [(&str, Vec<u8>); 4] = [
("an empty array", vec![0x90]),
("a trailing element", rmp_serde::to_vec(&(1u64, ("a",), 0u8)).unwrap()),
("a string key", rmp_serde::to_vec(&("CANARY-key", ("a",))).unwrap()),
("a unit arm with slots", rmp_serde::to_vec(&(2u64, ("CANARY-slot",))).unwrap()),
];
for (label, bytes) in cases {
let message = rmp_serde::from_slice::<Outcome>(&bytes).unwrap_err().to_string();
assert!(message.contains("Outcome"), "{label}: {message}");
assert!(!message.contains("CANARY"), "{label}: {message}");
}
}
#[test]
fn the_key_decides_the_arm() {
// Tool and Part carry different slots under different keys in the two unions.
let bytes = rmp_serde::to_vec(&(1u64, ("hammer", 3u8))).unwrap();
assert_eq!(
rmp_serde::from_slice::<Item>(&bytes).unwrap(),
Item::Tool(UsesArm { name: "hammer".into(), uses: 3 })
);
assert_eq!(
rmp_serde::from_slice::<CraftedItem>(&bytes).unwrap(),
CraftedItem::Part(CountArm { name: "hammer".into(), count: 3 })
);
}
#[test]
fn union_store_reads() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("union-surface.cc");
std::fs::write(&path, vector("union-surface.cc")).unwrap();
let mut cache = CultCache::new();
cache.register_entry_type::<UnionSurface>().unwrap();
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&path)).unwrap();
assert_eq!(cache.get::<UnionSurface>("surface").unwrap(), Some(constructed()));
}
mod serde_enum_bytes {
use cultcache_rs::DatabaseEntry;
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
enum External {
Unit,
Newtype(u32),
Struct { n: u8 },
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(tag = "kind")]
enum Internal {
A { n: u8 },
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(type = "tests.plain_enums")]
struct PlainEnums {
#[cultcache(key = 0)]
unit: External,
#[cultcache(key = 1)]
newtype: External,
#[cultcache(key = 2)]
structured: External,
#[cultcache(key = 3)]
tagged: Internal,
}
// Computed with the derive and serde at b0f40577, before CultUnion existed.
const AT_BASE: &[u8] = &[
0x94, 0xa4, b'U', b'n', b'i', b't', 0x81, 0xa7, b'N', b'e', b'w', b't', b'y', b'p', b'e', 0x05, 0x81,
0xa6, b'S', b't', b'r', b'u', b'c', b't', 0x91, 0x01, 0x92, 0xa1, b'A', 0x01,
];
#[test]
fn serde_enum_bytes_stable() {
let value = PlainEnums {
unit: External::Unit,
newtype: External::Newtype(5),
structured: External::Struct { n: 1 },
tagged: Internal::A { n: 1 },
};
let bytes = rmp_serde::to_vec(&value).unwrap();
assert_eq!(bytes, AT_BASE);
assert_eq!(rmp_serde::from_slice::<PlainEnums>(AT_BASE).unwrap(), value);
}
}