Skip to content

Commit eb71da7

Browse files
types: add Struct iteration, and Extend for Struct, ListValue and FieldMask (#712)
Fixes #704. Adds the collection items the issue lists, as one change: - `Struct`: `len`, `is_empty`, `contains_key`, `iter`, `IntoIterator` for `&Struct`, `&mut Struct` and `Struct`, and `Extend<(K, V)>` with the `Into<String>` / `Into<Value>` bounds of `from_fields`. A key that is already present has its value replaced, as `insert` does. The docs state that iteration order is unspecified. - `ListValue`: `Extend<V: Into<Value>>`, matching `from_values`. - `FieldMask`: `Extend<T: Into<String>>`, appending without normalizing, as `from_paths` does. Two additions beyond the plan I posted on the issue: - **`Struct::iter_mut`.** `IntoIterator for &mut Struct` needs a mutable iterator anyway, and `ListValue` pairs the two the same way. - **Three public iterator types, `StructIter`, `StructIterMut` and `StructIntoIter`,** re-exported at the crate root. `Struct::fields` is `buffa::__private::HashMap`, whose iterators are `std`'s or `hashbrown`'s depending on the `std` feature, so the `IntoIterator` impls need a type of their own to name. `serde_json::Map` takes the same approach. They forward `fold` and `count` to the map's iterators, implement `ExactSizeIterator` and `FusedIterator` (checked against the inner types in a test), and are `#[must_use]`. `FromIterator` keeps its existing item types (`(String, Value)` and `Value`). Widening it to match `Extend` could break inference in existing `collect` calls, so the crate docs describe the difference instead. Tests don't depend on hash order. One generic test checks that `C: Extend<T> + IntoIterator` works for all three types, which is the issue's motivation. **Verification** - `task lint`, `task doc`, `task check-nostd` and `task test-local` (4211 passed, 0 failed), with the repository-pinned protoc 33.5. `cargo +1.75 check -p buffa-types --all-features` with an MSRV-resolved lockfile, as the `msrv-check` job does. - Reviewed with the repo's `rust-code-reviewer` and `rust-api-ergonomics-reviewer` per CLAUDE.md: no Critical or High findings. Addressed the Mediums: the iterators forward `fold`/`count`, and the docs no longer overstate the `FromIterator`/`Extend` symmetry. Also addressed the Lows: a static check of the fused/exact-size claims, `#[must_use]`, `Extend` doc wording, and `HashMap::extend` delegation. --------- Co-authored-by: Iain McGinniss <iain@anthropic.com>
1 parent 47290ba commit eb71da7

4 files changed

Lines changed: 362 additions & 2 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
kind: Added
2+
body: |-
3+
**`Struct` gains `len`, `iter`, `IntoIterator` and related methods, and `Struct`, `ListValue` and `FieldMask` gain `Extend`** (#712, closes #704). `Struct` gains `len`, `is_empty`, `contains_key`, `iter`, `iter_mut`, `IntoIterator` for `&Struct`, `&mut Struct` and `Struct` (yielding the new `StructIter`, `StructIterMut` and `StructIntoIter`), and `Extend<(K, V)>` with the bounds of `from_fields`; an extended key that is already present takes the new value, as `insert` does. `ListValue` gains `Extend<V: Into<Value>>` and `FieldMask` gains `Extend<T: Into<String>>`, which appends without normalizing, as `from_paths` does. `FromIterator` keeps its existing item types (`(String, Value)` for `Struct`, `Value` for `ListValue`). `Struct::fields` is a hash map, so `Struct`'s iteration order is unspecified.
4+
time: 2026-10-13T08:58:19+08:00

‎buffa-types/src/field_mask_ext.rs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,14 @@ impl<T: Into<String>> FromIterator<T> for FieldMask {
270270
}
271271
}
272272

273+
impl<T: Into<String>> Extend<T> for FieldMask {
274+
/// Appends each path in iteration order, without validating or
275+
/// normalizing, as [`FieldMask::from_paths`] does.
276+
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
277+
self.paths.extend(iter.into_iter().map(Into::into));
278+
}
279+
}
280+
273281
impl<'a> IntoIterator for &'a FieldMask {
274282
type Item = &'a String;
275283
type IntoIter = core::slice::Iter<'a, String>;
@@ -404,6 +412,14 @@ mod tests {
404412
assert_eq!(mask, FieldMask::default());
405413
}
406414

415+
#[test]
416+
fn extend_appends_without_normalizing() {
417+
let mut mask = FieldMask::from_paths(["user"]);
418+
mask.extend(["user.name", "user"]);
419+
mask.extend(vec![String::from("settings")]);
420+
assert_eq!(mask.paths, ["user", "user.name", "user", "settings"]);
421+
}
422+
407423
#[test]
408424
fn from_paths_empty() {
409425
let mask = FieldMask::from_paths(core::iter::empty::<&str>());

‎buffa-types/src/lib.rs‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@
5050
//! - [`FieldMask`] path helpers: [`FieldMask::covers`], [`FieldMask::overlaps`],
5151
//! [`FieldMask::normalize`], [`FieldMask::union`], [`FieldMask::intersection`]
5252
//! - `Value` constructors: [`Value::null`](google::protobuf::Value::null), `From<f64>`, `From<String>`, `From<bool>`, etc.
53+
//! - Collection impls: [`Struct`], [`ListValue`] and [`FieldMask`] each have
54+
//! `len`, `is_empty`, `iter`, `IntoIterator`, `FromIterator` and `Extend`.
55+
//! `Extend` accepts anything `Into` the element type, while `FromIterator`
56+
//! on [`Struct`] and [`ListValue`] takes `(String, Value)` and [`Value`]
57+
//! items. [`Struct`] iterates through [`StructIter`], [`StructIterMut`] and
58+
//! [`StructIntoIter`], in unspecified order.
5359
//! - Wrapper type `From`/`Into` impls
5460
//!
5561
//! # Equality and hashing
@@ -237,10 +243,12 @@ pub use google::protobuf::{
237243
Any, Duration, Empty, FieldMask, ListValue, NullValue, Struct, Timestamp, Value,
238244
};
239245

240-
// Re-export error types from extension modules (these are hand-written types
241-
// in private modules, so re-exporting is the only way to make them accessible).
246+
// Re-export error and iterator types from extension modules (these are
247+
// hand-written types in private modules, so re-exporting is the only way to
248+
// make them accessible).
242249
pub use duration_ext::DurationError;
243250
pub use timestamp_ext::TimestampError;
251+
pub use value_ext::{StructIntoIter, StructIter, StructIterMut};
244252

245253
#[cfg(feature = "chrono")]
246254
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]

0 commit comments

Comments
 (0)