Skip to content

7971: stream: add StreamExt::filter_map_async#78

Open
martin-augment wants to merge 1 commit intomasterfrom
pr-7971-2026-03-13-08-50-04
Open

7971: stream: add StreamExt::filter_map_async#78
martin-augment wants to merge 1 commit intomasterfrom
pr-7971-2026-03-13-08-50-04

Conversation

@martin-augment
Copy link
Copy Markdown
Owner

7971: To review by AI

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the tokio-stream library by introducing StreamExt::filter_map_async. This new adapter provides a powerful way to asynchronously filter and map elements within a stream, allowing for more complex and flexible asynchronous data processing workflows. It enables users to apply async closures to stream items, where each item's processing is awaited before moving to the next, yielding Some(item) or skipping if None is returned.

Highlights

  • New Async Stream Adapter: Introduced StreamExt::filter_map_async, an asynchronous version of filter_map for Streams.
  • Asynchronous Closure Support: The new method allows filtering and mapping stream elements using an async closure, processing one future at a time.
  • New Module and Struct: Added filter_map_async.rs containing the FilterMapAsync struct, which implements the Stream trait for this new adapter.
Changelog
  • tokio-stream/src/lib.rs
    • Exported the new FilterMapAsync adapter from the stream_ext module.
  • tokio-stream/src/stream_ext.rs
    • Added a new module filter_map_async.
    • Exported the FilterMapAsync struct.
    • Implemented the filter_map_async method for the StreamExt trait, including its documentation and example.
  • tokio-stream/src/stream_ext/filter_map_async.rs
    • Added a new file defining the FilterMapAsync struct.
    • Implemented the Stream trait for FilterMapAsync, handling asynchronous polling and future management.
    • Included Debug implementation and a constructor for FilterMapAsync.
Activity
  • The pull request was created and is awaiting review.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 13, 2026

Walkthrough

This pull request adds a new asynchronous filter-map stream combinator to tokio-stream. The change introduces the FilterMapAsync type and a corresponding filter_map_async method to the StreamExt trait. The implementation includes a new module containing the FilterMapAsync struct, which applies an async function to each stream item, maintaining a single in-flight future at a time and emitting Some values while skipping None results. The new combinator is re-exported in the adapters module, making it part of the public API.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch pr-7971-2026-03-13-08-50-04
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces StreamExt::filter_map_async, a useful addition for asynchronous stream processing. The implementation is mostly solid. I've pointed out a couple of issues: one is a high-severity problem in the documentation example which uses syntax not available in the crate's MSRV, and the other is a medium-severity suggestion to improve code clarity in the implementation of poll_next. Once these are addressed, this will be a great contribution.

Comment on lines +510 to +512
/// let odds = stream.filter_map_async(async |x| {
/// if x % 2 == 0 { Some(x + 1) } else { None }
/// });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The async |x| { ... } syntax for async closures was stabilized in Rust 1.79. Given that tokio-stream has a Minimum Supported Rust Version (MSRV) of 1.71 (as per Cargo.toml), this example will fail to compile for users on the MSRV. Please use the |x| async move { ... } syntax which is compatible with older Rust versions.

Suggested change
/// let odds = stream.filter_map_async(async |x| {
/// if x % 2 == 0 { Some(x + 1) } else { None }
/// });
/// let odds = stream.filter_map_async(|x| async move {
/// if x % 2 == 0 { Some(x + 1) } else { None }
/// });

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:valid-but-wont-fix; category:bug; feedback: The Gemini AI reviewer is correct but since this is a doctest it is executed only with the nightly toolchain, so the build passes! Most of the Tokio users will use a recent toolchain so the example will work as is. The users who has to use older toolchain than 1.79 could easily adapt it

Comment on lines +55 to +60
if let Some(item) = ready!(future.poll(cx)) {
me.future.set(None);
return Poll::Ready(Some(item));
} else {
me.future.set(None);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The call to me.future.set(None) is duplicated in both branches of the if-else statement. This can be simplified by hoisting the call out to improve readability and reduce redundancy.

                let result = ready!(future.poll(cx));
                me.future.set(None);
                if let Some(item) = result {
                    return Poll::Ready(Some(item));
                }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:good-to-have; category:bug; feedback: The Gemini AI reviewer is correct! The same statement is executed in both 'if' and 'else' bodies, so it could be extracted before the if. It won't fix any bug but it will be easier to read and maintain

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.


fn size_hint(&self) -> (usize, Option<usize>) {
(0, self.stream.size_hint().1)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

size_hint upper bound ignores in-flight future

Low Severity

The size_hint upper bound doesn't account for an in-flight future. When an item has been consumed from the inner stream and a future is pending, self.stream.size_hint().1 no longer counts that item, but FilterMapAsync could still yield it if the future resolves to Some. This makes the reported upper bound potentially one less than the true maximum, violating the size_hint contract. The analogous Then adapter correctly adds future_len to the bounds.

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Bugbot AI reviewer is correct! The logic should take into account also the current future as the others combinators do. Since this is hint it does not need to be exact but it would be nice to use a more correct value if it is not too complex to calculate it.

///
/// let stream = stream::iter(0..=7);
/// let odds = stream.filter_map_async(async |x| {
/// if x % 2 == 0 { Some(x + 1) } else { None }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc example uses async closure syntax incompatible with MSRV

Medium Severity

The doc example uses async |x| { ... } (async closure syntax), which was stabilized in Rust 1.85. The crate declares rust-version = "1.71" in its Cargo.toml. This is the only use of async closure syntax in the entire tokio-stream crate. The analogous then method's doc example uses a named async fn instead. The idiomatic pre-1.85 pattern is |x| async move { ... }.

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:valid-but-wont-fix; category:bug; feedback: The Bugbot AI reviewer is correct but since this is a doctest it is executed only with the nightly toolchain, so the build passes! Most of the Tokio users will use a recent toolchain so the example will work as is. The users who has to use older toolchain than 1.79 could easily adapt it

@augmentcode
Copy link
Copy Markdown

augmentcode bot commented Mar 13, 2026

🤖 Augment PR Summary

Summary: Adds a new StreamExt::filter_map_async combinator to tokio-stream.

Changes:

  • Introduces a `FilterMapAsync` stream adapter that applies an async closure returning `Option`
  • Adds `StreamExt::filter_map_async` with docs and an example
  • Re-exports `FilterMapAsync` via `tokio_stream::adapters`

Technical Notes: The adapter runs at most one future at a time and may be !Unpin depending on the future type.

🤖 Was this summary useful? React with 👍 or 👎

Copy link
Copy Markdown

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

/// use tokio_stream::{self as stream, StreamExt};
///
/// let stream = stream::iter(0..=7);
/// let odds = stream.filter_map_async(async |x| {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example uses async |x| { ... } syntax, which won’t compile on Tokio’s MSRV (Rust 1.71). This can break rustdoc/doctests for filter_map_async’s docs.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:valid-but-wont-fix; category:bug; feedback: The Augment AI reviewer is correct but since this is a doctest it is executed only with the nightly toolchain, so the build passes! Most of the Tokio users will use a recent toolchain so the example will work as is. The users who has to use older toolchain than 1.79 could easily adapt it

}

fn size_hint(&self) -> (usize, Option<usize>) {
(0, self.stream.size_hint().1)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

size_hint()’s upper bound ignores whether self.future is currently Some, so it can under-report (e.g., stream upper is Some(0) but an in-flight future may still yield one item). That violates the size_hint upper-bound contract for streams.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Augment AI reviewer is correct! The logic should take into account also the current future as the others combinators do. Since this is hint it does not need to be exact but it would be nice to use a more correct value if it is not too complex to calculate it.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tokio-stream/src/stream_ext.rs`:
- Around line 510-512: The doctest snippet for filter_map_async uses the
unstable `async |x|` closure syntax; update the example to MSRV-compatible
syntax by replacing the `async |x| { ... }` closure with `|x| async move { ...
}` so code in the doctest (the example under stream.filter_map_async) compiles
on Rust 1.71–1.85; ensure you keep the same body logic and include `move` to
avoid borrow issues.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5467696f-7f8c-4561-a145-2d3db34d73a4

📥 Commits

Reviewing files that changed from the base of the PR and between a0ba4e8 and 91d9df3.

📒 Files selected for processing (3)
  • tokio-stream/src/lib.rs
  • tokio-stream/src/stream_ext.rs
  • tokio-stream/src/stream_ext/filter_map_async.rs

Comment on lines +510 to +512
/// let odds = stream.filter_map_async(async |x| {
/// if x % 2 == 0 { Some(x + 1) } else { None }
/// });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== rust-version declarations =="
fd '^Cargo.toml$' -a | while read -r f; do
  if rg -n '^\s*rust-version\s*=' "$f" >/dev/null; then
    echo "-- $f"
    rg -n '^\s*(name|version|rust-version)\s*=' "$f"
  fi
done

echo
echo "== async-closure syntax occurrences in tokio-stream =="
rg -nP '\basync\s*\|' tokio-stream/src

Repository: martin-augment/tokio

Length of output: 835


🌐 Web query:

Rust async closure syntax stabilization version

💡 Result:

Rust’s async closure syntax (e.g., async |args| { ... } / async move |args| { ... }) was stabilized in Rust 1.85.0, released February 20, 2025. [1], [2]

Before 1.85, it was only available on nightly behind #![feature(async_closure)] (tracking issue: rust-lang/rust #62290). [3]

Sources:
[1] Rust Blog: “Announcing Rust 1.85.0 and Rust 2024” (2025-02-20)
[2] rust-lang/rust RELEASES.md (1.85.0): “Stabilize async closures”
[3] rust-lang/rust GitHub: Tracking issue #62290 (feature(async_closure))


Use MSRV-compatible closure syntax in the doctest example.

The async |x| syntax at line 510 is incompatible with tokio-stream's MSRV of 1.71. This syntax was only stabilized in Rust 1.85.0; it is not available in the supported range. Use |x| async move { ... } instead, which is supported across all MSRV-compatible versions.

Suggested fix
-/// let odds = stream.filter_map_async(async |x| {
+/// let odds = stream.filter_map_async(|x| async move {
 ///     if x % 2 == 0 { Some(x + 1) } else { None }
 /// });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// let odds = stream.filter_map_async(async |x| {
/// if x % 2 == 0 { Some(x + 1) } else { None }
/// });
/// let odds = stream.filter_map_async(|x| async move {
/// if x % 2 == 0 { Some(x + 1) } else { None }
/// });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tokio-stream/src/stream_ext.rs` around lines 510 - 512, The doctest snippet
for filter_map_async uses the unstable `async |x|` closure syntax; update the
example to MSRV-compatible syntax by replacing the `async |x| { ... }` closure
with `|x| async move { ... }` so code in the doctest (the example under
stream.filter_map_async) compiles on Rust 1.71–1.85; ensure you keep the same
body logic and include `move` to avoid borrow issues.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:valid-but-wont-fix; category:bug; feedback: The CodeRabbit AI reviewer is correct but since this is a doctest it is executed only with the nightly toolchain, so the build passes! Most of the Tokio users will use a recent toolchain so the example will work as is. The users who has to use older toolchain than 1.79 could easily adapt it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants