Skip to content

Commit 5740352

Browse files
lpahlaviclaude
andcommitted
feat(storable): implement Storable for unbounded 2-tuples
Replaces the two `todo!()` panics in the `(A, B)` `Storable` impl (`into_bytes_inner_2` and `from_bytes`) with a working implementation for unbounded tuples. The bounded 2-tuple wire format is unchanged. Wire format for unbounded `(A, B)`, documented in a comment inside the impl block mirroring the 3-tuple style: If A is fixed-size: <a_bytes> <b_bytes> Otherwise: <size_lengths (1B)> <size_a (1-4B)> <a_bytes> <b_bytes> `size_lengths` encodes (in 2 bits) the number of bytes used to store `size_a`, matching the encoding already used by the 3-tuple path. `B`'s length is always inferred from the remaining bytes. Tests mirror the 3-tuple unbounded test coverage: - `tuple_with_two_unbounded_elements_roundtrip` - `tuple_with_two_elements_bounded_and_unbounded_roundtrip` (with byte count) - `optional_tuple_with_two_unbounded_elements_roundtrip` - `optional_tuple_with_two_elements_bounded_and_unbounded_roundtrip` - `tuple_with_two_elements_test_bound` Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1198b43 commit 5740352

2 files changed

Lines changed: 101 additions & 3 deletions

File tree

src/storable/tests.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@ proptest! {
2222
prop_assert_eq!(tuple, Storable::from_bytes(bytes));
2323
}
2424

25+
#[test]
26+
fn tuple_with_two_unbounded_elements_roundtrip(v1 in pvec(any::<u8>(), 0..4), v2 in pvec(any::<u8>(), 0..8)) {
27+
let tuple = (v1, v2);
28+
assert_eq!(tuple, Storable::from_bytes(tuple.to_bytes()));
29+
}
30+
31+
#[test]
32+
fn tuple_with_two_elements_bounded_and_unbounded_roundtrip(x in pvec(any::<u8>(), 4..5), y in any::<u64>()) {
33+
let tuple = (x, y);
34+
let tuple_copy = tuple.clone();
35+
let bytes = tuple_copy.to_bytes();
36+
// 1B sizes len | 1B x size | 4B x bytes | 8B y bytes
37+
prop_assert_eq!(bytes.len(), 14);
38+
prop_assert_eq!(tuple, Storable::from_bytes(bytes));
39+
}
40+
2541
#[test]
2642
fn tuple_with_three_unbounded_elements_roundtrip(v1 in pvec(any::<u8>(), 0..4), v2 in pvec(any::<u8>(), 0..8), v3 in pvec(any::<u8>(), 0..12)) {
2743
let tuple = (v1, v2, v3);
@@ -38,7 +54,6 @@ proptest! {
3854
prop_assert_eq!(tuple, Storable::from_bytes(bytes));
3955
}
4056

41-
4257
#[test]
4358
fn tuple_variable_width_u8_roundtrip(x in any::<u64>(), v in pvec(any::<u8>(), 0..40)) {
4459
let bytes = Blob::<48>::try_from(&v[..]).unwrap();
@@ -100,6 +115,11 @@ proptest! {
100115
prop_assert_eq!(v, Storable::from_bytes(v.to_bytes()));
101116
}
102117

118+
#[test]
119+
fn optional_tuple_with_two_unbounded_elements_roundtrip(v in proptest::option::of((pvec(any::<u8>(), 0..4), pvec(any::<u8>(), 0..8)))) {
120+
prop_assert_eq!(v.clone(), Storable::from_bytes(v.to_bytes()));
121+
}
122+
103123
#[test]
104124
fn optional_tuple_with_three_unbounded_elements_roundtrip(v in proptest::option::of((pvec(any::<u8>(), 0..4), pvec(any::<u8>(), 0..8), pvec(any::<u8>(), 0..12)))) {
105125
prop_assert_eq!(v.clone(), Storable::from_bytes(v.to_bytes()));
@@ -117,6 +137,11 @@ proptest! {
117137
prop_assert_eq!(v, Storable::from_bytes(v.to_bytes()));
118138
}
119139

140+
#[test]
141+
fn optional_tuple_with_two_elements_bounded_and_unbounded_roundtrip(v in proptest::option::of((any::<u64>(), pvec(any::<u8>(), 0..40)))) {
142+
prop_assert_eq!(v.clone(), Storable::from_bytes(v.to_bytes()));
143+
}
144+
120145
#[test]
121146
fn optional_tuple_with_three_elements_bounded_and_unbounded_roundtrip(v in proptest::option::of((any::<u64>(), pvec(any::<u8>(), 0..40), pvec(any::<u8>(), 0..80)))) {
122147
prop_assert_eq!(v.clone(), Storable::from_bytes(v.to_bytes()));
@@ -240,6 +265,29 @@ fn storable_for_bool() {
240265
assert!(bool::from_bytes(true.to_bytes()));
241266
}
242267

268+
#[test]
269+
fn tuple_with_two_elements_test_bound() {
270+
// <8B a_bytes> <8B b_bytes>
271+
assert_eq!(<(u64, u64)>::BOUND.max_size(), 16);
272+
assert!(<(u64, u64)>::BOUND.is_fixed_size());
273+
274+
// <8B a_bytes> <8B b_bytes (zero-padded)> <1B size_b>
275+
assert_eq!(<(u64, Blob<8>)>::BOUND.max_size(), 17);
276+
assert!(!<(u64, Blob<8>)>::BOUND.is_fixed_size());
277+
278+
// <8B a_bytes (zero-padded)> <8B b_bytes> <1B size_a>
279+
assert_eq!(<(Blob<8>, u64)>::BOUND.max_size(), 17);
280+
assert!(!<(Blob<8>, u64)>::BOUND.is_fixed_size());
281+
282+
// <8B a_bytes (zero-padded)> <8B b_bytes (zero-padded)> <1B size_a> <1B size_b>
283+
assert_eq!(<(Blob<8>, Blob<8>)>::BOUND.max_size(), 18);
284+
assert!(!<(Blob<8>, Blob<8>)>::BOUND.is_fixed_size());
285+
286+
assert_eq!(<(Blob<8>, String)>::BOUND, Bound::Unbounded);
287+
assert_eq!(<(String, Blob<8>)>::BOUND, Bound::Unbounded);
288+
assert_eq!(<(String, String)>::BOUND, Bound::Unbounded);
289+
}
290+
243291
#[test]
244292
fn tuple_with_three_elements_test_bound() {
245293
// <8B a_bytes> <8B b_bytes> <8B c_bytes>

src/storable/tuples.rs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ where
88
A: Storable,
99
B: Storable,
1010
{
11+
// Tuple (A, B) serialization:
12+
// Bounded (A and B are both bounded):
13+
// <a_bytes (max_a B, zero-padded)> <b_bytes (max_b B, zero-padded)> <size_a (0-4B)> <size_b (0-4B)>
14+
// Unbounded, when A is fixed-size:
15+
// <a_bytes> <b_bytes>
16+
// Unbounded, otherwise:
17+
// <size_lengths (1B)> <size_a (1-4B)> <a_bytes> <b_bytes>
1118
fn to_bytes(&self) -> Cow<[u8]> {
1219
match Self::BOUND {
1320
Bound::Bounded { max_size, .. } => {
@@ -41,7 +48,33 @@ where
4148

4249
Cow::Owned(bytes)
4350
}
44-
_ => todo!("Serializing tuples with unbounded types is not yet supported."),
51+
Bound::Unbounded => {
52+
let a_bytes = self.0.to_bytes();
53+
let b_bytes = self.1.to_bytes();
54+
let a_size = a_bytes.len();
55+
let b_size = b_bytes.len();
56+
57+
let sizes_overhead = if A::BOUND.is_fixed_size() {
58+
0
59+
} else {
60+
1 + bytes_to_store_size(a_size)
61+
};
62+
63+
let output_size = a_size + b_size + sizes_overhead;
64+
let mut bytes = vec![0; output_size];
65+
let mut offset = 0;
66+
67+
if sizes_overhead != 0 {
68+
bytes[offset] = encode_size_lengths(&[a_size]);
69+
offset += 1;
70+
}
71+
72+
offset += encode_tuple_element::<A>(&mut bytes[offset..], a_bytes.as_ref(), false);
73+
offset += encode_tuple_element::<B>(&mut bytes[offset..], b_bytes.as_ref(), true);
74+
75+
debug_assert_eq!(offset, output_size);
76+
Cow::Owned(bytes)
77+
}
4578
}
4679
}
4780

@@ -71,7 +104,24 @@ where
71104
let b = B::from_bytes(Cow::Borrowed(&bytes[a_max_size..a_max_size + b_len]));
72105
(a, b)
73106
}
74-
_ => todo!("Deserializing tuples with unbounded types is not yet supported."),
107+
Bound::Unbounded => {
108+
let mut offset = 0;
109+
let mut size_length_a = None;
110+
111+
if !A::BOUND.is_fixed_size() {
112+
let lengths = decode_size_lengths(bytes[0], 1);
113+
offset += 1;
114+
size_length_a = Some(lengths[0]);
115+
}
116+
117+
let (a, read) = decode_tuple_element::<A>(&bytes[offset..], size_length_a, false);
118+
offset += read;
119+
let (b, read) = decode_tuple_element::<B>(&bytes[offset..], None, true);
120+
offset += read;
121+
122+
debug_assert_eq!(offset, bytes.len());
123+
(a, b)
124+
}
75125
}
76126
}
77127

0 commit comments

Comments
 (0)