Skip to content

Commit f0f1ef7

Browse files
committed
bd-proto-util: Use Default for missing map entry fields per protobuf spec
Per protobuf specification, when a map entry is missing its key or value field, the type's default value should be used instead of returning an error. Changes: - Add Default bounds to deserialize_map_entry generic parameters - Use unwrap_or_default() instead of returning errors for missing fields - Add Default bounds to HashMap/AHashMap ProtoFieldDeserialize impls - Update documentation to reflect protobuf-compatible behavior This allows map types to support any value type that implements Default. For types without Default (like OffsetDateTime), users should wrap them in Option<T> so that None is the default, avoiding epoch timestamps.
1 parent 612f88b commit f0f1ef7

File tree

17 files changed

+2656
-11
lines changed

17 files changed

+2656
-11
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ members = [
2424
"bd-logger",
2525
"bd-log-filter",
2626
"bd-log-matcher",
27+
"bd-macros",
2728
"bd-internal-logging",
2829
"bd-network-quality",
2930
"bd-noop-network",

bd-log-primitives/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ doctest = false
1212
ahash.workspace = true
1313
anyhow.workspace = true
1414
bd-proto = { path = "../bd-proto" }
15+
bd-proto-util = { path = "../bd-proto-util" }
1516
bd-workspace-hack.workspace = true
1617
flate2 = { workspace = true, default-features = false, features = ["zlib"] }
1718
ordered-float.workspace = true

bd-log-primitives/src/lib.rs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use crate::zlib::DEFAULT_MOBILE_ZLIB_COMPRESSION_LEVEL;
2828
use ahash::AHashMap;
2929
use bd_proto::protos::logging::payload::data::Data_type;
3030
use bd_proto::protos::logging::payload::{BinaryData, Data, LogType};
31+
use bd_proto_util::serialization::{ProtoFieldSerialize, TimestampMicros};
3132
use flate2::Compression;
3233
use flate2::write::ZlibEncoder;
3334
use ordered_float::NotNan;
@@ -143,6 +144,7 @@ impl StringOrBytes<String, Vec<u8>> {
143144

144145
impl<T: AsRef<str>, B: AsRef<[u8]>> StringOrBytes<T, B> {
145146
/// Extracts the underlying str if the enum represents a String, None otherwise.
147+
#[must_use]
146148
pub fn as_str(&self) -> Option<&str> {
147149
match self {
148150
Self::String(s) => Some(s.as_ref()),
@@ -153,6 +155,7 @@ impl<T: AsRef<str>, B: AsRef<[u8]>> StringOrBytes<T, B> {
153155
}
154156

155157
/// Extracts the underlying bytes if the enum represents a Bytes, None otherwise.
158+
#[must_use]
156159
pub fn as_bytes(&self) -> Option<&[u8]> {
157160
match self {
158161
Self::String(_)
@@ -572,8 +575,10 @@ impl LogEncodingHelper {
572575

573576
// uint64 timestamp_unix_micro = 1;
574577
// No zero check is this should always be set.
575-
my_size +=
576-
::protobuf::rt::uint64_size(1, occurred_at.unix_timestamp_nanos().to_u64_lossy() / 1000);
578+
// We manually serialize this as a uint64 (microsecond timestamp) to match the proto
579+
// definition.
580+
let micros = TimestampMicros::new(occurred_at).as_micros();
581+
my_size += protobuf::rt::uint64_size(1, micros);
577582

578583
// uint32 log_level = 2;
579584
if log_level != 0 {
@@ -585,9 +590,7 @@ impl LogEncodingHelper {
585590
my_size += ::protobuf::rt::string_size(5, session_id);
586591

587592
// LogType log_type = 7;
588-
if log_type != LogType::NORMAL {
589-
my_size += ::protobuf::rt::int32_size(7, log_type as i32);
590-
}
593+
my_size += <LogType as ProtoFieldSerialize>::compute_size(&log_type, 7);
591594

592595
*cached_encoding_data = Some(CachedEncodingData {
593596
core_size: my_size,
@@ -646,7 +649,10 @@ impl LogEncodingHelper {
646649
cached_encoding_data: Option<&CachedEncodingData>,
647650
) -> anyhow::Result<()> {
648651
// uint64 timestamp_unix_micro = 1;
649-
os.write_uint64(1, occurred_at.unix_timestamp_nanos().to_u64_lossy() / 1000)?;
652+
// We manually serialize this as a uint64 (microsecond timestamp) to match the proto definition.
653+
let micros = TimestampMicros::new(occurred_at).as_micros();
654+
os.write_tag(1, protobuf::rt::WireType::Varint)?;
655+
os.write_uint64_no_tag(micros)?;
650656

651657
// uint32 log_level = 2;
652658
if log_level != 0 {
@@ -674,9 +680,7 @@ impl LogEncodingHelper {
674680
}
675681

676682
// LogType log_type = 7;
677-
if log_type != LogType::NORMAL {
678-
os.write_enum(7, log_type as i32)?;
679-
}
683+
<LogType as ProtoFieldSerialize>::serialize(&log_type, 7, os)?;
680684

681685
// repeated string stream_ids = 8;
682686
for v in stream_ids {
@@ -734,7 +738,9 @@ impl LogEncodingHelper {
734738
if raw_tag == 8 // field number 1, wire type 0
735739
&& let Some(ts_micros) = cis.read_uint64().ok()
736740
{
737-
return OffsetDateTime::from_unix_timestamp_nanos((ts_micros * 1000).into()).ok();
741+
return TimestampMicros::from_micros(ts_micros)
742+
.ok()
743+
.map(TimestampMicros::into_inner);
738744
}
739745

740746
None

bd-log-primitives/src/tiny_set.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,20 @@ impl<T: PartialEq> FromIterator<T> for TinySet<T> {
9999
}
100100
}
101101

102+
impl<T: PartialEq> Extend<T> for TinySet<T> {
103+
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
104+
self.inner.extend(iter.into_iter().map(|item| (item, ())));
105+
}
106+
}
107+
108+
impl<T: PartialEq> Extend<Self> for TinySet<T> {
109+
fn extend<I: IntoIterator<Item = Self>>(&mut self, iter: I) {
110+
for set in iter {
111+
self.inner.extend(set.inner);
112+
}
113+
}
114+
}
115+
102116
impl<T: PartialEq, const N: usize> From<[T; N]> for TinySet<T> {
103117
fn from(arr: [T; N]) -> Self {
104118
Self {
@@ -119,6 +133,18 @@ impl<T> IntoIterator for TinySet<T> {
119133
}
120134
}
121135

136+
impl<'a, T> IntoIterator for &'a TinySet<T> {
137+
type Item = &'a T;
138+
type IntoIter = std::iter::Map<std::slice::Iter<'a, (T, ())>, fn(&'a (T, ())) -> &'a T>;
139+
140+
fn into_iter(self) -> Self::IntoIter {
141+
fn map_ref<T>((k, ()): &(T, ())) -> &T {
142+
k
143+
}
144+
self.inner.items.iter().map(map_ref)
145+
}
146+
}
147+
122148
//
123149
// TinyMap
124150
//
@@ -129,6 +155,10 @@ pub struct TinyMap<K, V> {
129155
}
130156

131157
impl<K: PartialEq, V> TinyMap<K, V> {
158+
pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
159+
self.items.iter().map(|(k, v)| (k, v))
160+
}
161+
132162
pub fn get<Q>(&self, key: &Q) -> Option<&V>
133163
where
134164
K: Borrow<Q>,
@@ -209,6 +239,18 @@ impl<K, V> IntoIterator for TinyMap<K, V> {
209239
}
210240
}
211241

242+
impl<'a, K, V> IntoIterator for &'a TinyMap<K, V> {
243+
type Item = (&'a K, &'a V);
244+
type IntoIter = std::iter::Map<std::slice::Iter<'a, (K, V)>, fn(&'a (K, V)) -> (&'a K, &'a V)>;
245+
246+
fn into_iter(self) -> Self::IntoIter {
247+
fn map_ref<K, V>((k, v): &(K, V)) -> (&K, &V) {
248+
(k, v)
249+
}
250+
self.items.iter().map(map_ref)
251+
}
252+
}
253+
212254
impl<K: PartialEq, V> FromIterator<(K, V)> for TinyMap<K, V> {
213255
fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
214256
let mut new = Self::default();
@@ -225,8 +267,69 @@ impl<K: PartialEq, V, const N: usize> From<[(K, V); N]> for TinyMap<K, V> {
225267
}
226268
}
227269

270+
impl<K: PartialEq, V> Extend<(K, V)> for TinyMap<K, V> {
271+
fn extend<I: IntoIterator<Item = (K, V)>>(&mut self, iter: I) {
272+
for (k, v) in iter {
273+
self.insert(k, v);
274+
}
275+
}
276+
}
277+
278+
impl<K: PartialEq, V> Extend<Self> for TinyMap<K, V> {
279+
fn extend<I: IntoIterator<Item = Self>>(&mut self, iter: I) {
280+
for map in iter {
281+
for (k, v) in map {
282+
self.insert(k, v);
283+
}
284+
}
285+
}
286+
}
287+
228288
impl<K, V> Default for TinyMap<K, V> {
229289
fn default() -> Self {
230290
Self { items: Vec::new() }
231291
}
232292
}
293+
294+
// ProtoFieldSerialize/ProtoFieldDeserialize implementations for TinySet<T>
295+
// TinySet is serialized as a repeated field in protobuf.
296+
bd_proto_util::impl_proto_repeated!(TinySet<T>, T, PartialEq);
297+
298+
// ProtoFieldSerialize/ProtoFieldDeserialize implementations for TinyMap
299+
// TinyMap is serialized as a map field (repeated key-value pairs) in protobuf.
300+
impl<K, V> bd_proto_util::serialization::ProtoType for TinyMap<K, V> {
301+
fn wire_type() -> protobuf::rt::WireType {
302+
protobuf::rt::WireType::LengthDelimited
303+
}
304+
}
305+
306+
impl<K, V> bd_proto_util::serialization::ProtoFieldSerialize for TinyMap<K, V>
307+
where
308+
K: bd_proto_util::serialization::ProtoFieldSerialize + PartialEq,
309+
V: bd_proto_util::serialization::ProtoFieldSerialize,
310+
{
311+
fn compute_size(&self, field_number: u32) -> u64 {
312+
bd_proto_util::serialization::compute_map_size(self, field_number)
313+
}
314+
315+
fn serialize(
316+
&self,
317+
field_number: u32,
318+
os: &mut protobuf::CodedOutputStream<'_>,
319+
) -> anyhow::Result<()> {
320+
bd_proto_util::serialization::serialize_map(self, field_number, os)
321+
}
322+
}
323+
324+
impl<K, V> bd_proto_util::serialization::ProtoFieldDeserialize for TinyMap<K, V>
325+
where
326+
K: bd_proto_util::serialization::ProtoFieldDeserialize + PartialEq + Default,
327+
V: bd_proto_util::serialization::ProtoFieldDeserialize + Default,
328+
{
329+
fn deserialize(is: &mut protobuf::CodedInputStream<'_>) -> anyhow::Result<Self> {
330+
let (key, value) = bd_proto_util::serialization::deserialize_map_entry(is)?;
331+
let mut map = Self::default();
332+
map.extend(std::iter::once((key, value)));
333+
Ok(map)
334+
}
335+
}

bd-macros/Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
edition = "2024"
3+
license-file = "../LICENSE"
4+
name = "bd-macros"
5+
publish = false
6+
version = "1.0.0"
7+
8+
[lib]
9+
doctest = false
10+
proc-macro = true
11+
12+
[dependencies]
13+
bd-workspace-hack.workspace = true
14+
proc-macro2 = "1.0"
15+
quote = "1.0"
16+
syn = { version = "2.0", features = ["full"] }

0 commit comments

Comments
 (0)