Skip to content

Commit

Permalink
Add Lender type for easily iterating over List builders
Browse files Browse the repository at this point in the history
A simple utility to help with iterating over List builders. Normally you'd
use a Range and write a manual for loop to access element builders, but it
doesn't work as well with other Iterators. Lender and ZippedLender provide
simple utilities to work with both. While they don't support Iterator
itself, we can still use `while let` with them which is close enough...
  • Loading branch information
ObsidianMinor committed Oct 2, 2024
1 parent 90821e3 commit af6f589
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 1 deletion.
71 changes: 70 additions & 1 deletion recapn/src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use crate::{Error, Family, IntoFamily, Result};

use core::convert::TryFrom;
use core::marker::PhantomData;
use core::ops::Range;
use core::ops::{Bound, Range, RangeBounds};

pub use crate::ptr::{ElementCount, ElementSize};

Expand Down Expand Up @@ -302,6 +302,75 @@ impl<'a, V, T: Table> Builder<'a, V, T> {
{
V::get(self, index)
}

/// Create a lending iterator for the whole list.
#[inline]
pub fn into_lender(self) -> Lender<Self> {
self.into_lender_range(..)
}

/// Create a lending iterator for a specified range in the list.
#[inline]
pub fn into_lender_range<R: RangeBounds<u32>>(self, range: R) -> Lender<Self> {
let len = self.len().get();
let start = match range.start_bound() {
Bound::Unbounded => 0,
Bound::Included(i) => *i,
Bound::Excluded(i) => (*i)
.min(ElementCount::MAX_VALUE)
.saturating_add(1),
}.min(len);
let end = match range.end_bound() {
Bound::Unbounded => len,
Bound::Included(i) => (*i)
.min(ElementCount::MAX_VALUE)
.saturating_add(1),
Bound::Excluded(i) => *i,
}.min(len);
Lender { range: start..end, list: self }
}
}

/// A lending iterator for list builders.
pub struct Lender<L> {
range: Range<u32>,
list: L,
}

impl<L> Lender<L> {
/// Zip this lender with a normal `Iterator`
pub fn zip<I: IntoIterator>(self, iter: I) -> ZippedLender<L, I::IntoIter> {
ZippedLender { lender: self, iter: iter.into_iter() }
}
}

impl<'a, V, T: Table> Lender<Builder<'a, V, T>> {
pub fn next<'b>(&'b mut self) -> Option<ElementBuilder<'a, 'b, V, T>>
where
V: ListAccessable<&'b mut Builder<'a, V, T>>
{
let idx = self.range.next()?;
let list_item = self.list.at(idx);
Some(list_item)
}
}

/// A lender with an iterator. Useful for when you need to copy from a Rust type to a
/// Cap'n Proto list.
pub struct ZippedLender<L, I> {
lender: Lender<L>,
iter: I,
}

impl<'a, V, T: Table, I: Iterator> ZippedLender<Builder<'a, V, T>, I> {
pub fn next<'b>(&'b mut self) -> Option<(ElementBuilder<'a, 'b, V, T>, I::Item)>
where
V: ListAccessable<&'b mut Builder<'a, V, T>>
{
let list_item = self.lender.next()?;
let iter_item = self.iter.next()?;
Some((list_item, iter_item))
}
}

/// An element in a list reader
Expand Down
2 changes: 2 additions & 0 deletions tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ pub mod build_gen {
include!(concat!(env!("OUT_DIR"), "/mod.rs"));
}

pub mod list;

use gen::capnp_test_capnp::{TestAllTypes, TestEnum};
use recapn::message::Message;
use recapn::text;
Expand Down
58 changes: 58 additions & 0 deletions tests/src/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use crate::gen::capnp_test_capnp::{TestAllTypes, TestEnum};
use recapn::message::Message;
use recapn::ty::AsReader;

#[test]
fn test_lender() {
// Lender primarily exists for copying from native types to Cap'n Proto types. Lists are
// especially painful to deal with because it requires using enumerator() and that sucks.
// Lender gives a nicer API that, while it doesn't support `for` syntax, does support
// `while let` which is the next best thing.

struct Data {
value: i8,
big_value: u64,
enum_value: TestEnum,
}

let some_data = vec![
Data { value: 5, big_value: 0, enum_value: TestEnum::Qux },
Data { value: -1, big_value: 0xFFFFFFFFFFFFF, enum_value: TestEnum::Bar },
];

let mut message = Message::global();
let mut builder = message.builder().init_struct_root::<TestAllTypes>();
let mut list = builder.struct_list()
.init(some_data.len() as u32)
.into_lender()
.zip(&some_data);
while let Some((builder, data)) = list.next() {
let mut e = builder.get();
e.int8_field().set(data.value);
e.u_int64_field().set(data.big_value);
e.enum_field().set(data.enum_value);
}

let list = builder.as_reader().struct_list().get();
assert_eq!(list.len(), some_data.len() as u32);

for (e, data) in list.into_iter().zip(&some_data) {
assert_eq!(e.int8_field(), data.value);
assert_eq!(e.u_int64_field(), data.big_value);
assert_eq!(e.enum_field().unwrap(), data.enum_value);
}

// Check to make sure other types of ranges work.
let mut list = builder.bool_list().init(6).into_lender_range(2..=3);
while let Some(mut b) = list.next() {
b.set(true);
}

let list = builder.as_reader().bool_list().get();
assert_eq!(list.len(), 6);
assert!(list.into_iter().eq([false, false, true, true, false, false].into_iter()));

// Going into ranges out of bounds yields no elements.
let mut list = builder.u_int8_list().init(3).into_lender_range(4..);
assert!(list.next().is_none());
}

0 comments on commit af6f589

Please sign in to comment.