├── .github └── workflows │ └── checks.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── cliff.toml ├── examples ├── file_numbers.rs └── numbers.rs └── src └── lib.rs /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main] 4 | pull_request: 5 | types: [opened, reopened, synchronize] 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 8 | cancel-in-progress: true 9 | name: checks 10 | jobs: 11 | fmt: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Install rust (stable) 16 | uses: dtolnay/rust-toolchain@stable 17 | with: 18 | components: rustfmt 19 | - run: cargo fmt --check 20 | clippy: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Install rust (stable) 25 | uses: dtolnay/rust-toolchain@stable 26 | with: 27 | components: clippy 28 | - run: cargo clippy --all-targets --workspace -- --deny warnings 29 | test: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Install rust (stable) 34 | uses: dtolnay/rust-toolchain@stable 35 | - run: cargo test --all-targets --workspace 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.2.2] - 2024-04-23 6 | 7 | ### 🚀 Features 8 | 9 | - Implement emit_err for try_fn_stream 10 | 11 | ### 🚜 Refactor 12 | 13 | - Use internal_emit instead of duplicate code 14 | 15 | ### 📚 Documentation 16 | 17 | - Improve documentation and some panic messages 18 | 19 | ### 🧪 Testing 20 | 21 | - Add test for emit_err 22 | 23 | ## [0.2.1] - 2024-04-18 24 | 25 | ### 🚜 Refactor 26 | 27 | - Replace futures by futures-util and futures-executor 28 | 29 | ### 📚 Documentation 30 | 31 | - Replace futures by futures-util 32 | 33 | ### 🧪 Testing 34 | 35 | - Remove unnecessary import 36 | 37 | 38 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "async-fn-stream" 3 | description = "Lightweight implementation of `async-stream` without macros" 4 | version = "0.2.2" 5 | edition = "2021" 6 | license = "MIT" 7 | homepage = "https://github.com/dmitryvk/async-fn-stream" 8 | repository = "https://github.com/dmitryvk/async-fn-stream" 9 | keywords = ["async", "stream"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | futures-util = { version = "0.3", default-features = false, features = ["std"] } 15 | pin-project-lite = "0.2" 16 | 17 | [dev-dependencies] 18 | anyhow = "1" 19 | futures-executor = { version = "0.3", default-features = false, features = ["std", "thread-pool"] } 20 | tokio = { version = "1.0", features = ["full"] } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Dmitry Kalyanov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A version of [async-stream](https://github.com/tokio-rs/async-stream) without macros. 2 | This crate provides generic implementations of `Stream` trait. 3 | `Stream` is an asynchronous version of `std::iter::Iterator`. 4 | 5 | Two functions are provided - `fn_stream` and `try_fn_stream`. 6 | 7 | # Usage 8 | 9 | ## Basic Usage 10 | 11 | If you need to create a stream that may result in error, use `try_fn_stream`, otherwise use `fn_stream`. 12 | 13 | To create a stream: 14 | 15 | 1. Invoke `fn_stream` or `try_fn_stream`, passing a closure (anonymous function). 16 | 2. Closure will accept an `emitter`. 17 | To return value from the stream, call `.emit(value)` on `emitter` and `.await` on its result. 18 | Once stream consumer has processed the value and called `.next()` on stream, `.await` will return. 19 | 20 | ## Returning errors 21 | 22 | `try_fn_stream` provides some conveniences for returning errors: 23 | 24 | 1. Errors can be return from closure via `return Err(...)` or the question mark (`?`) operator. 25 | This will end the stream. 26 | 2. An `emitter` also has an `emit_err()` method to return errors without ending the stream. 27 | 28 | # Examples 29 | 30 | Finite stream of numbers 31 | 32 | ```rust 33 | use async_fn_stream::fn_stream; 34 | use futures_util::Stream; 35 | 36 | fn build_stream() -> impl Stream { 37 | fn_stream(|emitter| async move { 38 | for i in 0..3 { 39 | // yield elements from stream via `emitter` 40 | emitter.emit(i).await; 41 | } 42 | }) 43 | } 44 | ``` 45 | 46 | Read numbers from text file, with error handling 47 | 48 | ```rust 49 | use anyhow::Context; 50 | use async_fn_stream::try_fn_stream; 51 | use futures_util::{pin_mut, Stream, StreamExt}; 52 | use tokio::{ 53 | fs::File, 54 | io::{AsyncBufReadExt, BufReader}, 55 | }; 56 | 57 | fn read_numbers(file_name: String) -> impl Stream> { 58 | try_fn_stream(|emitter| async move { 59 | // Return errors via `?` operator. 60 | let file = BufReader::new(File::open(file_name).await.context("Failed to open file")?); 61 | pin_mut!(file); 62 | let mut line = String::new(); 63 | loop { 64 | line.clear(); 65 | let byte_count = file 66 | .read_line(&mut line) 67 | .await 68 | .context("Failed to read line")?; 69 | if byte_count == 0 { 70 | break; 71 | } 72 | 73 | for token in line.split_ascii_whitespace() { 74 | let Ok(number) = token.parse::() else { 75 | // Return errors via the `emit_err` method. 76 | emitter.emit_err( 77 | anyhow::anyhow!("Failed to convert string \"{token}\" to number") 78 | ).await; 79 | continue; 80 | }; 81 | emitter.emit(number).await; 82 | } 83 | } 84 | 85 | Ok(()) 86 | }) 87 | } 88 | ``` 89 | 90 | # Why not `async-stream`? 91 | 92 | [async-stream](https://github.com/tokio-rs/async-stream) is great! 93 | It has a nice syntax, but it is based on macros which brings some flaws: 94 | * proc-macros sometimes interacts badly with IDEs such as rust-analyzer or IntelliJ Rust. 95 | see e.g. 96 | * proc-macros may increase build times 97 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ default configuration file 2 | # https://git-cliff.org/docs/configuration 3 | # 4 | # Lines starting with "#" are comments. 5 | # Configuration options are organized into tables and keys. 6 | # See documentation for more information on available options. 7 | 8 | [changelog] 9 | # changelog header 10 | header = """ 11 | # Changelog\n 12 | All notable changes to this project will be documented in this file.\n 13 | """ 14 | # template for the changelog body 15 | # https://keats.github.io/tera/docs/#introduction 16 | body = """ 17 | {% if version %}\ 18 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 19 | {% else %}\ 20 | ## [unreleased] 21 | {% endif %}\ 22 | {% for group, commits in commits | group_by(attribute="group") %} 23 | ### {{ group | striptags | trim | upper_first }} 24 | {% for commit in commits %} 25 | - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ 26 | {% if commit.breaking %}[**breaking**] {% endif %}\ 27 | {{ commit.message | upper_first }}\ 28 | {% endfor %} 29 | {% endfor %}\n 30 | """ 31 | # template for the changelog footer 32 | footer = """ 33 | 34 | """ 35 | # remove the leading and trailing s 36 | trim = true 37 | # postprocessors 38 | postprocessors = [ 39 | # { pattern = '', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL 40 | ] 41 | 42 | [git] 43 | # parse the commits based on https://www.conventionalcommits.org 44 | conventional_commits = true 45 | # filter out the commits that are not conventional 46 | filter_unconventional = true 47 | # process each line of a commit as an individual commit 48 | split_commits = false 49 | # regex for preprocessing the commit messages 50 | commit_preprocessors = [ 51 | # Replace issue numbers 52 | #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, 53 | # Check spelling of the commit with https://github.com/crate-ci/typos 54 | # If the spelling is incorrect, it will be automatically fixed. 55 | #{ pattern = '.*', replace_command = 'typos --write-changes -' }, 56 | ] 57 | # regex for parsing and grouping commits 58 | commit_parsers = [ 59 | { message = "^feat", group = "🚀 Features" }, 60 | { message = "^fix", group = "🐛 Bug Fixes" }, 61 | { message = "^doc", group = "📚 Documentation" }, 62 | { message = "^perf", group = "⚡ Performance" }, 63 | { message = "^refactor", group = "🚜 Refactor" }, 64 | { message = "^style", group = "🎨 Styling" }, 65 | { message = "^test", group = "🧪 Testing" }, 66 | { message = "^chore\\(release\\): prepare for", skip = true }, 67 | { message = "^chore\\(deps.*\\)", skip = true }, 68 | { message = "^chore\\(pr\\)", skip = true }, 69 | { message = "^chore\\(pull\\)", skip = true }, 70 | { message = "^chore|^ci", group = "⚙️ Miscellaneous Tasks" }, 71 | { body = ".*security", group = "🛡️ Security" }, 72 | { message = "^revert", group = "◀️ Revert" }, 73 | ] 74 | # protect breaking changes from being skipped due to matching a skipping commit_parser 75 | protect_breaking_commits = false 76 | # filter out the commits that are not matched by commit parsers 77 | filter_commits = false 78 | # regex for matching git tags 79 | # tag_pattern = "v[0-9].*" 80 | # regex for skipping tags 81 | # skip_tags = "" 82 | # regex for ignoring tags 83 | # ignore_tags = "" 84 | # sort the tags topologically 85 | topo_order = false 86 | # sort the commits inside sections by oldest/newest order 87 | sort_commits = "oldest" 88 | # limit the number of commits included in the changelog. 89 | # limit_commits = 42 90 | -------------------------------------------------------------------------------- /examples/file_numbers.rs: -------------------------------------------------------------------------------- 1 | use std::env::args; 2 | 3 | use anyhow::Context; 4 | use async_fn_stream::try_fn_stream; 5 | use futures_util::{pin_mut, Stream, StreamExt}; 6 | use tokio::{ 7 | fs::File, 8 | io::{AsyncBufReadExt, BufReader}, 9 | }; 10 | 11 | #[tokio::main] 12 | async fn main() -> Result<(), anyhow::Error> { 13 | let file_name = args().nth(1).unwrap(); 14 | let stream = read_numbers(file_name); 15 | pin_mut!(stream); 16 | while let Some(number) = stream.next().await { 17 | println!("number: {}", number?); 18 | } 19 | 20 | Ok(()) 21 | } 22 | 23 | fn read_numbers(file_name: String) -> impl Stream> { 24 | try_fn_stream(|emitter| async move { 25 | let file = BufReader::new(File::open(file_name).await.context("Failed to open file")?); 26 | pin_mut!(file); 27 | let mut line = String::new(); 28 | loop { 29 | line.clear(); 30 | let byte_count = file 31 | .read_line(&mut line) 32 | .await 33 | .context("Failed to read line")?; 34 | if byte_count == 0 { 35 | break; 36 | } 37 | 38 | for token in line.split_ascii_whitespace() { 39 | let number: i32 = token 40 | .parse() 41 | .with_context(|| format!("Failed to conver string \"{token}\" to number"))?; 42 | emitter.emit(number).await; 43 | } 44 | } 45 | 46 | Ok(()) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /examples/numbers.rs: -------------------------------------------------------------------------------- 1 | use async_fn_stream::fn_stream; 2 | use futures_util::{pin_mut, Stream, StreamExt}; 3 | 4 | fn build_stream() -> impl Stream { 5 | fn_stream(|emitter| async move { 6 | for i in 0..3 { 7 | // yield elements from stream via `collector` 8 | emitter.emit(i).await; 9 | } 10 | }) 11 | } 12 | 13 | async fn example() { 14 | let stream = build_stream(); 15 | 16 | pin_mut!(stream); 17 | let mut numbers = Vec::new(); 18 | while let Some(number) = stream.next().await { 19 | print!("{} ", number); 20 | numbers.push(number); 21 | } 22 | println!(); 23 | assert_eq!(numbers, vec![0, 1, 2]); 24 | } 25 | 26 | pub fn main() { 27 | futures_executor::block_on(example()); 28 | } 29 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | use std::{ 4 | pin::Pin, 5 | sync::{Arc, Mutex}, 6 | task::{Poll, Waker}, 7 | }; 8 | 9 | use futures_util::{Future, FutureExt, Stream}; 10 | use pin_project_lite::pin_project; 11 | 12 | /// An intermediary that transfers values from stream to its consumer 13 | pub struct StreamEmitter { 14 | inner: Arc>>, 15 | } 16 | 17 | /// An intermediary that transfers values from stream to its consumer 18 | pub struct TryStreamEmitter { 19 | inner: Arc>>>, 20 | } 21 | 22 | struct Inner { 23 | value: Option, 24 | waker: Option, 25 | } 26 | 27 | pin_project! { 28 | /// Implementation of [`Stream`] trait created by [`fn_stream`]. 29 | pub struct FnStream> { 30 | #[pin] 31 | fut: Fut, 32 | inner: Arc>>, 33 | } 34 | } 35 | 36 | /// Create a new infallible stream which is implemented by `func`. 37 | /// 38 | /// Caller should pass an async function which will return successive stream elements via [`StreamEmitter::emit`]. 39 | /// 40 | /// # Example 41 | /// 42 | /// ```rust 43 | /// use async_fn_stream::fn_stream; 44 | /// use futures_util::Stream; 45 | /// 46 | /// fn build_stream() -> impl Stream { 47 | /// fn_stream(|emitter| async move { 48 | /// for i in 0..3 { 49 | /// // yield elements from stream via `emitter` 50 | /// emitter.emit(i).await; 51 | /// } 52 | /// }) 53 | /// } 54 | /// ``` 55 | pub fn fn_stream>( 56 | func: impl FnOnce(StreamEmitter) -> Fut, 57 | ) -> FnStream { 58 | FnStream::new(func) 59 | } 60 | 61 | impl> FnStream { 62 | fn new) -> Fut>(func: F) -> Self { 63 | let inner = Arc::new(Mutex::new(Inner { 64 | value: None, 65 | waker: None, 66 | })); 67 | let emitter = StreamEmitter { 68 | inner: inner.clone(), 69 | }; 70 | let fut = func(emitter); 71 | Self { fut, inner } 72 | } 73 | } 74 | 75 | impl> Stream for FnStream { 76 | type Item = T; 77 | 78 | fn poll_next( 79 | self: Pin<&mut Self>, 80 | cx: &mut std::task::Context<'_>, 81 | ) -> Poll> { 82 | let mut this = self.project(); 83 | 84 | this.inner.lock().expect("Mutex was poisoned").waker = Some(cx.waker().clone()); 85 | let r = this.fut.poll_unpin(cx); 86 | match r { 87 | std::task::Poll::Ready(()) => Poll::Ready(None), 88 | std::task::Poll::Pending => { 89 | let value = this.inner.lock().expect("Mutex was poisoned").value.take(); 90 | match value { 91 | None => Poll::Pending, 92 | Some(value) => Poll::Ready(Some(value)), 93 | } 94 | } 95 | } 96 | } 97 | } 98 | 99 | /// Create a new fallible stream which is implemented by `func`. 100 | /// 101 | /// Caller should pass an async function which can: 102 | /// 103 | /// - return successive stream elements via [`StreamEmitter::emit`] 104 | /// - return transient errors via [`StreamEmitter::emit_err`] 105 | /// - return fatal errors as [`Result::Err`] 106 | /// 107 | /// # Example 108 | /// ```rust 109 | /// use async_fn_stream::try_fn_stream; 110 | /// use futures_util::Stream; 111 | /// 112 | /// fn build_stream() -> impl Stream> { 113 | /// try_fn_stream(|emitter| async move { 114 | /// for i in 0..3 { 115 | /// // yield elements from stream via `emitter` 116 | /// emitter.emit(i).await; 117 | /// } 118 | /// 119 | /// // return errors view emitter without ending the stream 120 | /// emitter.emit_err(anyhow::anyhow!("An error happened")); 121 | /// 122 | /// // return errors from stream, ending the stream 123 | /// Err(anyhow::anyhow!("An error happened")) 124 | /// }) 125 | /// } 126 | /// ``` 127 | pub fn try_fn_stream>>( 128 | func: impl FnOnce(TryStreamEmitter) -> Fut, 129 | ) -> TryFnStream { 130 | TryFnStream::new(func) 131 | } 132 | 133 | pin_project! { 134 | /// Implementation of [`Stream`] trait created by [`try_fn_stream`]. 135 | pub struct TryFnStream>> { 136 | is_err: bool, 137 | #[pin] 138 | fut: Fut, 139 | inner: Arc>>>, 140 | } 141 | } 142 | 143 | impl>> TryFnStream { 144 | fn new) -> Fut>(func: F) -> Self { 145 | let inner = Arc::new(Mutex::new(Inner { 146 | value: None, 147 | waker: None, 148 | })); 149 | let emitter = TryStreamEmitter { 150 | inner: inner.clone(), 151 | }; 152 | let fut = func(emitter); 153 | Self { 154 | is_err: false, 155 | fut, 156 | inner, 157 | } 158 | } 159 | } 160 | 161 | impl>> Stream for TryFnStream { 162 | type Item = Result; 163 | 164 | fn poll_next( 165 | self: Pin<&mut Self>, 166 | cx: &mut std::task::Context<'_>, 167 | ) -> Poll> { 168 | if self.is_err { 169 | return Poll::Ready(None); 170 | } 171 | let mut this = self.project(); 172 | this.inner.lock().expect("Mutex was poisoned").waker = Some(cx.waker().clone()); 173 | let r = this.fut.poll_unpin(cx); 174 | match r { 175 | std::task::Poll::Ready(Ok(())) => Poll::Ready(None), 176 | std::task::Poll::Ready(Err(e)) => { 177 | *this.is_err = true; 178 | Poll::Ready(Some(Err(e))) 179 | } 180 | std::task::Poll::Pending => { 181 | let value = this.inner.lock().expect("Mutex was poisoned").value.take(); 182 | match value { 183 | None => Poll::Pending, 184 | Some(value) => Poll::Ready(Some(value)), 185 | } 186 | } 187 | } 188 | } 189 | } 190 | 191 | impl StreamEmitter { 192 | /// Emit value from a stream and wait until stream consumer calls [`futures_util::StreamExt::next`] again. 193 | /// 194 | /// # Panics 195 | /// Will panic if: 196 | /// * `emit` is called twice without awaiting result of first call 197 | /// * `emit` is called not in context of polling the stream 198 | #[must_use = "Ensure that emit() is awaited"] 199 | pub fn emit(&self, value: T) -> CollectFuture { 200 | let mut inner = self.inner.lock().expect("Mutex was poisoned"); 201 | let inner = &mut *inner; 202 | if inner.value.is_some() { 203 | panic!("StreamEmitter::emit() was called without `.await`'ing result of previous emit") 204 | } 205 | inner.value = Some(value); 206 | inner 207 | .waker 208 | .take() 209 | .expect("StreamEmitter::emit() should only be called in context of Future::poll()") 210 | .wake(); 211 | CollectFuture { polled: false } 212 | } 213 | } 214 | 215 | impl TryStreamEmitter { 216 | fn internal_emit(&self, res: Result) -> CollectFuture { 217 | let mut inner = self.inner.lock().expect("Mutex was poisoned"); 218 | let inner = &mut *inner; 219 | if inner.value.is_some() { 220 | panic!( 221 | "TreStreamEmitter::emit/emit_err() was called without `.await`'ing result of previous collect" 222 | ) 223 | } 224 | inner.value = Some(res); 225 | inner 226 | .waker 227 | .take() 228 | .expect("TreStreamEmitter::emit/emit_err() should only be called in context of Future::poll()") 229 | .wake(); 230 | CollectFuture { polled: false } 231 | } 232 | 233 | /// Emit value from a stream and wait until stream consumer calls [`futures_util::StreamExt::next`] again. 234 | /// 235 | /// # Panics 236 | /// Will panic if: 237 | /// * `emit`/`emit_err` is called twice without awaiting result of the first call 238 | /// * `emit` is called not in context of polling the stream 239 | #[must_use = "Ensure that emit() is awaited"] 240 | pub fn emit(&self, value: T) -> CollectFuture { 241 | self.internal_emit(Ok(value)) 242 | } 243 | 244 | /// Emit error from a stream and wait until stream consumer calls [`futures_util::StreamExt::next`] again. 245 | /// 246 | /// # Panics 247 | /// Will panic if: 248 | /// * `emit`/`emit_err` is called twice without awaiting result of the first call 249 | /// * `emit_err` is called not in context of polling the stream 250 | #[must_use = "Ensure that emit_err() is awaited"] 251 | pub fn emit_err(&self, err: E) -> CollectFuture { 252 | self.internal_emit(Err(err)) 253 | } 254 | } 255 | 256 | /// Future returned from [`StreamEmitter::emit`]. 257 | pub struct CollectFuture { 258 | polled: bool, 259 | } 260 | 261 | impl Future for CollectFuture { 262 | type Output = (); 263 | 264 | fn poll(self: Pin<&mut Self>, _cx: &mut std::task::Context<'_>) -> Poll { 265 | if self.polled { 266 | Poll::Ready(()) 267 | } else { 268 | self.get_mut().polled = true; 269 | Poll::Pending 270 | } 271 | } 272 | } 273 | 274 | #[cfg(test)] 275 | mod tests { 276 | use std::io::ErrorKind; 277 | 278 | use futures_util::{pin_mut, StreamExt}; 279 | 280 | use super::*; 281 | 282 | #[test] 283 | fn infallible_works() { 284 | futures_executor::block_on(async { 285 | let stream = fn_stream(|collector| async move { 286 | eprintln!("stream 1"); 287 | collector.emit(1).await; 288 | eprintln!("stream 2"); 289 | collector.emit(2).await; 290 | eprintln!("stream 3"); 291 | }); 292 | pin_mut!(stream); 293 | assert_eq!(Some(1), stream.next().await); 294 | assert_eq!(Some(2), stream.next().await); 295 | assert_eq!(None, stream.next().await); 296 | }); 297 | } 298 | 299 | #[test] 300 | fn infallible_lifetime() { 301 | let a = 1; 302 | futures_executor::block_on(async { 303 | let b = 2; 304 | let a = &a; 305 | let b = &b; 306 | let stream = fn_stream(|collector| async move { 307 | eprintln!("stream 1"); 308 | collector.emit(a).await; 309 | eprintln!("stream 2"); 310 | collector.emit(b).await; 311 | eprintln!("stream 3"); 312 | }); 313 | pin_mut!(stream); 314 | assert_eq!(Some(a), stream.next().await); 315 | assert_eq!(Some(b), stream.next().await); 316 | assert_eq!(None, stream.next().await); 317 | }); 318 | } 319 | 320 | #[test] 321 | #[should_panic] 322 | fn infallible_panics_on_multiple_collects() { 323 | futures_executor::block_on(async { 324 | #[allow(unused_must_use)] 325 | let stream = fn_stream(|collector| async move { 326 | eprintln!("stream 1"); 327 | collector.emit(1); 328 | collector.emit(2); 329 | eprintln!("stream 3"); 330 | }); 331 | pin_mut!(stream); 332 | assert_eq!(Some(1), stream.next().await); 333 | assert_eq!(Some(2), stream.next().await); 334 | assert_eq!(None, stream.next().await); 335 | }); 336 | } 337 | 338 | #[test] 339 | fn fallible_works() { 340 | futures_executor::block_on(async { 341 | let stream = try_fn_stream(|collector| async move { 342 | eprintln!("try stream 1"); 343 | collector.emit(1).await; 344 | eprintln!("try stream 2"); 345 | collector.emit(2).await; 346 | eprintln!("try stream 3"); 347 | Err(std::io::Error::from(ErrorKind::Other)) 348 | }); 349 | pin_mut!(stream); 350 | assert_eq!(1, stream.next().await.unwrap().unwrap()); 351 | assert_eq!(2, stream.next().await.unwrap().unwrap()); 352 | assert!(stream.next().await.unwrap().is_err()); 353 | assert!(stream.next().await.is_none()); 354 | }); 355 | } 356 | 357 | #[test] 358 | fn fallible_emit_err_works() { 359 | futures_executor::block_on(async { 360 | let stream = try_fn_stream(|collector| async move { 361 | eprintln!("try stream 1"); 362 | collector.emit(1).await; 363 | eprintln!("try stream 2"); 364 | collector.emit(2).await; 365 | eprintln!("try stream 3"); 366 | collector 367 | .emit_err(std::io::Error::from(ErrorKind::Other)) 368 | .await; 369 | eprintln!("try stream 4"); 370 | Err(std::io::Error::from(ErrorKind::Other)) 371 | }); 372 | pin_mut!(stream); 373 | assert_eq!(1, stream.next().await.unwrap().unwrap()); 374 | assert_eq!(2, stream.next().await.unwrap().unwrap()); 375 | assert!(stream.next().await.unwrap().is_err()); 376 | assert!(stream.next().await.unwrap().is_err()); 377 | assert!(stream.next().await.is_none()); 378 | }); 379 | } 380 | 381 | #[test] 382 | fn method_async() { 383 | struct St { 384 | a: String, 385 | } 386 | 387 | impl St { 388 | async fn f1(&self) -> impl Stream { 389 | self.f2().await 390 | } 391 | 392 | async fn f2(&self) -> impl Stream { 393 | fn_stream(|collector| async move { 394 | collector.emit(self.a.as_str()).await; 395 | collector.emit(self.a.as_str()).await; 396 | collector.emit(self.a.as_str()).await; 397 | }) 398 | } 399 | } 400 | 401 | futures_executor::block_on(async { 402 | let l = St { 403 | a: "qwe".to_owned(), 404 | }; 405 | let s = l.f1().await; 406 | let z: Vec<&str> = s.collect().await; 407 | assert_eq!(z, ["qwe", "qwe", "qwe"]); 408 | }) 409 | } 410 | } 411 | --------------------------------------------------------------------------------