diff --git a/Cargo.lock b/Cargo.lock index b4af2bd7..1a08929d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -419,6 +419,7 @@ dependencies = [ "itertools 0.14.0", "lazy-regex", "macro_rules_attribute", + "tokio", "typed-builder", "walkdir", ] @@ -975,9 +976,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.46.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "io-uring", diff --git a/bon-macros/Cargo.toml b/bon-macros/Cargo.toml index 45df795b..493f944c 100644 --- a/bon-macros/Cargo.toml +++ b/bon-macros/Cargo.toml @@ -63,6 +63,9 @@ rustversion = "1.0" [features] default = [] +alloc = [] +std = [] + # See the docs on this feature in the `bon`'s crate `Cargo.toml` experimental-overwritable = [] diff --git a/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs b/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs new file mode 100644 index 00000000..ecd869ce --- /dev/null +++ b/bon-macros/src/builder/builder_gen/builder_derives/into_future.rs @@ -0,0 +1,215 @@ +use crate::builder::builder_gen::models::BuilderGenCtx; +use crate::builder::builder_gen::top_level_config::IntoFutureConfig; +use crate::util::prelude::*; +use std::borrow::Cow; +use std::collections::BTreeSet; +use syn::visit_mut::VisitMut; + +impl BuilderGenCtx { + pub(super) fn derive_into_future(&self, config: &IntoFutureConfig) -> Result { + if self.finish_fn.asyncness.is_none() { + // While it is technically possible to call a synchronous function + // inside of the `IntoFuture::into_future()`, it's better force the + // user to mark the function as `async` explicitly. Otherwise it may + // indicate of some logic bug where the developer mistakenly marks + // a function that could be sync with `derive(IntoFuture)`. + bail!( + &self.finish_fn.ident, + "`#[builder(derive(IntoFuture(...)))` can only be used with async functions; \ + using it with a synchronous function is likely a mistake" + ); + } + + if let Some(unsafety) = &self.finish_fn.unsafety { + bail!( + unsafety, + "`#[builder(derive(IntoFuture(...)))` is not supported for unsafe functions \ + because `IntoFuture::into_future()` method is a safe method" + ); + } + + if let Some(arg) = self.finish_fn_args().next() { + bail!( + &arg.config.finish_fn.span(), + "`#[builder(derive(IntoFuture(...)))` is incompatible with `#[builder(finish_fn)]` members \ + because `IntoFuture::into_future()` method accepts zero parameters" + ); + } + + let state_mod = &self.state_mod.ident; + let builder_ident = &self.builder_type.ident; + let state_var = &self.state_var; + let finish_fn_ident = &self.finish_fn.ident; + let box_ = &config.box_ident; + + let SignatureForIntoFuture { + generics_decl, + generic_args, + where_clause, + builder_lifetime, + output_ty, + } = self.signature_for_into_future(); + + let state_lifetime = builder_lifetime + .clone() + .unwrap_or_else(|| syn::Lifetime::new("'static", Span::call_site())); + + let builder_lifetime = Option::into_iter(builder_lifetime); + + let send_bound = if config.is_send { + quote! { + ::core::marker::Send } + } else { + quote! {} + }; + + let bon = &self.bon; + + let alloc = if cfg!(feature = "std") { + quote!(::std) + } else if cfg!(feature = "alloc") { + quote!(#bon::__::alloc) + } else { + bail!( + &config.box_ident, + "`#[builder(derive(IntoFuture(Box)))]` requires either `std` or \ + `alloc` feature to be enabled" + ) + }; + + let tokens = quote! { + #[automatically_derived] + impl< + #(#generics_decl,)* + #state_var: #state_mod::IsComplete + #state_lifetime + > + ::core::future::IntoFuture for #builder_ident<#(#generic_args,)* #state_var> + #where_clause + { + type Output = #output_ty; + type IntoFuture = ::core::pin::Pin< + #alloc::boxed::#box_< + dyn ::core::future::Future + #send_bound + #(+ #builder_lifetime)* + > + >; + + fn into_future(self) -> Self::IntoFuture { + #alloc::boxed::#box_::pin(#builder_ident::#finish_fn_ident(self)) + } + } + }; + + Ok(tokens) + } + + /// Handle the special case for a builder that captures lifetimes. + /// + /// Collapse all lifetimes into a single `'builder` lifetime. This is + /// because `dyn Trait` supports only a single `+ 'lifetime` bound. + fn signature_for_into_future(&self) -> SignatureForIntoFuture<'_> { + let generics_decl = &self.generics.decl_without_defaults; + let generic_args = &self.generics.args; + let where_clause = &self.generics.where_clause; + + let output_ty = match &self.finish_fn.output { + syn::ReturnType::Default => Cow::Owned(syn::parse_quote!(())), + syn::ReturnType::Type(_, output_ty) => Cow::Borrowed(output_ty.as_ref()), + }; + + let contains_lifetimes = matches!( + self.generics.args.first(), + Some(syn::GenericArgument::Lifetime(_)) + ); + + if !contains_lifetimes { + return SignatureForIntoFuture { + generics_decl: Cow::Borrowed(generics_decl), + generic_args: Cow::Borrowed(generic_args), + where_clause: where_clause.as_ref().map(Cow::Borrowed), + builder_lifetime: None, + output_ty, + }; + } + + let builder_lifetime = syn::Lifetime::new("'builder", Span::call_site()); + + let new_generic_args = generic_args + .iter() + .map(|arg| match arg { + syn::GenericArgument::Lifetime(_) => { + syn::GenericArgument::Lifetime(builder_lifetime.clone()) + } + _ => arg.clone(), + }) + .collect::>(); + + let mut original_lifetimes = BTreeSet::new(); + let mut new_generics_decl = vec![syn::parse_quote!(#builder_lifetime)]; + + for param in generics_decl { + match param { + syn::GenericParam::Lifetime(lifetime) => { + original_lifetimes.insert(&lifetime.lifetime.ident); + } + _ => { + new_generics_decl.push(param.clone()); + } + } + } + + let mut replace_lifetimes = ReplaceLifetimes { + replacement: &builder_lifetime, + original_lifetimes: &original_lifetimes, + }; + + let mut new_where_clause = where_clause.clone(); + + if let Some(where_clause) = &mut new_where_clause { + replace_lifetimes.visit_where_clause_mut(where_clause); + } + + let mut output_ty = output_ty.into_owned(); + + replace_lifetimes.visit_type_mut(&mut output_ty); + + SignatureForIntoFuture { + generics_decl: Cow::Owned(new_generics_decl), + generic_args: Cow::Owned(new_generic_args), + where_clause: new_where_clause.map(Cow::Owned), + builder_lifetime: Some(builder_lifetime), + output_ty: Cow::Owned(output_ty), + } + } +} + +struct SignatureForIntoFuture<'a> { + generics_decl: Cow<'a, [syn::GenericParam]>, + generic_args: Cow<'a, [syn::GenericArgument]>, + where_clause: Option>, + builder_lifetime: Option, + output_ty: Cow<'a, syn::Type>, +} + +struct ReplaceLifetimes<'a> { + replacement: &'a syn::Lifetime, + original_lifetimes: &'a BTreeSet<&'a syn::Ident>, +} + +impl VisitMut for ReplaceLifetimes<'_> { + fn visit_lifetime_mut(&mut self, lifetime: &mut syn::Lifetime) { + if self.original_lifetimes.contains(&lifetime.ident) { + *lifetime = self.replacement.clone(); + } + } + + fn visit_item_mut(&mut self, _: &mut syn::Item) { + // Don't recurse into child items. They don't inherit the parent item's + // lifetimes. + } + + fn visit_bound_lifetimes_mut(&mut self, _: &mut syn::BoundLifetimes) { + // Don't recurse into bound lifetime declarations. They introduce + // local lifetimes that we should keep as is + } +} diff --git a/bon-macros/src/builder/builder_gen/builder_derives/mod.rs b/bon-macros/src/builder/builder_gen/builder_derives/mod.rs index 573169ba..df886000 100644 --- a/bon-macros/src/builder/builder_gen/builder_derives/mod.rs +++ b/bon-macros/src/builder/builder_gen/builder_derives/mod.rs @@ -1,6 +1,7 @@ mod clone; mod debug; mod into; +mod into_future; use super::top_level_config::{DeriveConfig, DerivesConfig}; use super::BuilderGenCtx; @@ -9,7 +10,12 @@ use darling::ast::GenericParamExt; impl BuilderGenCtx { pub(crate) fn builder_derives(&self) -> Result { - let DerivesConfig { clone, debug, into } = &self.builder_type.derives; + let DerivesConfig { + clone, + debug, + into, + into_future, + } = &self.builder_type.derives; let mut tokens = TokenStream::new(); @@ -25,6 +31,10 @@ impl BuilderGenCtx { tokens.extend(self.derive_into()?); } + if let Some(derive) = into_future { + tokens.extend(self.derive_into_future(derive)?); + } + Ok(tokens) } diff --git a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs index 24230bb3..c9507cde 100644 --- a/bon-macros/src/builder/builder_gen/top_level_config/mod.rs +++ b/bon-macros/src/builder/builder_gen/top_level_config/mod.rs @@ -194,6 +194,9 @@ pub(crate) struct DerivesConfig { #[darling(rename = "Into")] pub(crate) into: darling::util::Flag, + + #[darling(rename = "IntoFuture")] + pub(crate) into_future: Option, } #[derive(Debug, Clone, Default)] @@ -201,6 +204,69 @@ pub(crate) struct DeriveConfig { pub(crate) bounds: Option>, } +#[derive(Debug, Clone)] +pub(crate) struct IntoFutureConfig { + pub(crate) box_ident: syn::Ident, + pub(crate) is_send: bool, +} + +impl syn::parse::Parse for IntoFutureConfig { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + // Parse "Box" as the required first argument. + let box_ident: syn::Ident = input.parse()?; + if box_ident != "Box" { + return Err(syn::Error::new( + box_ident.span(), + "expected `Box` as the first argument, only boxed futures are supported", + )); + } + + // Check for optional ", ?Send" part. + let is_send = if input.peek(syn::Token![,]) { + input.parse::()?; + + // Parse "?Send" as a single unit. + if input.peek(syn::Token![?]) { + input.parse::()?; + let send_ident: syn::Ident = input.parse()?; + if send_ident != "Send" { + return Err(syn::Error::new( + send_ident.span(), + "expected `Send` after ?", + )); + } + false + } else { + return Err(input.error("expected `?Send` as the second argument")); + } + } else { + true + }; + + // Ensure no trailing tokens. + if !input.is_empty() { + return Err(input.error("unexpected tokens after arguments")); + } + + Ok(Self { box_ident, is_send }) + } +} + +impl FromMeta for IntoFutureConfig { + fn from_meta(meta: &syn::Meta) -> Result { + let meta = match meta { + syn::Meta::List(meta) => meta, + _ => bail!(meta, "expected an attribute of form `IntoFuture(Box, ...)`"), + }; + + meta.require_parens_delim()?; + + let me = syn::parse2(meta.tokens.clone())?; + + Ok(me) + } +} + impl FromMeta for DeriveConfig { fn from_meta(meta: &syn::Meta) -> Result { if let syn::Meta::Path(_) = meta { diff --git a/bon/Cargo.toml b/bon/Cargo.toml index dd235ff9..7536eab1 100644 --- a/bon/Cargo.toml +++ b/bon/Cargo.toml @@ -61,9 +61,9 @@ macro_rules_attribute = "0.2" trybuild = "1.0.89" [features] -alloc = [] +alloc = ["bon-macros/alloc"] default = ["std"] -std = ["alloc"] +std = ["bon-macros/std", "alloc"] # See the explanation of what this feature does in the docs here: # https://bon-rs.com/guide/typestate-api/custom-methods#implied-bounds diff --git a/bon/src/__/ide.rs b/bon/src/__/ide.rs index a0809f81..404b40a7 100644 --- a/bon/src/__/ide.rs +++ b/bon/src/__/ide.rs @@ -99,6 +99,10 @@ pub mod builder_top_level { /// See the docs at pub use core::convert::Into; + + /// See the docs at + #[rustversion::since(1.64)] + pub use core::future::IntoFuture; } /// The real name of this parameter is `crate` (without the underscore). diff --git a/bon/tests/integration/builder/attr_into_future.rs b/bon/tests/integration/builder/attr_into_future.rs new file mode 100644 index 00000000..171c0bc1 --- /dev/null +++ b/bon/tests/integration/builder/attr_into_future.rs @@ -0,0 +1,257 @@ +/// [`core::future::IntoFuture`] relies on [`Box`]. Also this trait was +/// introduced in Rust 1.64, while `bon`'s MSRV is 1.59 at the time of this +/// writing. +#[cfg(any(feature = "std", feature = "alloc"))] +#[rustversion::since(1.64)] +mod tests { + use crate::prelude::*; + use core::future::{ready, IntoFuture}; + use core::marker::PhantomData; + + async fn assert_send(builder: B) -> B::Output + where + B: IntoFuture + Send, + B::IntoFuture: Send, + { + #[expect(clippy::incompatible_msrv)] + let fut = builder.into_future(); + let _: &dyn Send = &fut; + fut.await + } + + #[expect(clippy::future_not_send)] + async fn non_send_future() { + // By keeping `Rc` across an await point, we force the compiler to store it + // as part of the future's state machine struct and thus we make it non-Send + let non_send = PhantomData::>; + + ready(()).await; + + let _ = &non_send; + } + + mod test_fn { + use super::*; + + #[tokio::test] + async fn basic() { + #[builder(derive(IntoFuture(Box)))] + async fn simple_async_fn(value: u32) -> u32 { + ready(value * 2).await + } + + // Test direct call. + let builder = simple_async_fn().value(21).call(); + assert_eq!(assert_send(builder).await, 42); + + // Test using IntoFuture with await. + let builder = simple_async_fn().value(21); + assert_eq!(assert_send(builder).await, 42); + } + + #[tokio::test] + async fn non_send() { + #[builder(derive(IntoFuture(Box, ?Send)))] + #[expect(clippy::future_not_send)] + async fn non_send_async_fn(value: u32) -> u32 { + non_send_future().await; + // This future can be !Send. + ready(value * 2).await + } + + // Test with non-Send future. + let result = non_send_async_fn().value(21).await; + + assert_eq!(result, 42); + } + + #[tokio::test] + async fn result() { + #[builder(derive(IntoFuture(Box)))] + async fn async_with_result(value: u32) -> Result { + ready(if value > 0 { + Ok(value * 2) + } else { + Err("Value must be positive") + }) + .await + } + + // Test successful case. + let builder = async_with_result().value(21); + assert_eq!(assert_send(builder).await.unwrap(), 42); + + // Test error case. + let builder = async_with_result().value(0); + + assert_send(builder).await.unwrap_err(); + } + + #[tokio::test] + async fn into_future_with_optional() { + #[builder(derive(IntoFuture(Box)))] + async fn optional_param(#[builder(default = 100)] value: u32) -> u32 { + ready(value).await + } + + // Test with value. + let builder = optional_param().value(42); + assert_eq!(assert_send(builder).await, 42); + + // Test without value (using default). + let builder = optional_param(); + assert_eq!(assert_send(builder).await, 100); + } + + #[tokio::test] + async fn references_in_params() { + struct Dummy; + + #[builder(derive(IntoFuture(Box)))] + async fn sut<'named1, 'named2>( + _x1: &Dummy, + _x2: &Dummy, + x3: &'named1 Dummy, + x4: &'named2 Dummy, + ) -> &'named2 Dummy { + let _: &'named1 Dummy = x3; + ready(x4).await + } + + // Store the dummy struct in local variables to make sure no `'static` + // lifetime promotion happens + let local_x1 = Dummy; + let local_x2 = Dummy; + let local_x3 = Dummy; + let local_x4 = Dummy; + + let builder = sut() + .x1(&local_x1) + .x2(&local_x2) + .x3(&local_x3) + .x4(&local_x4); + + let &Dummy = assert_send(builder).await; + } + + #[tokio::test] + async fn anon_lifetime_in_return_type() { + struct Dummy; + + #[builder(derive(IntoFuture(Box)))] + async fn sut(x1: &Dummy) -> &Dummy { + ready(x1).await + } + + // Store the dummy struct in local variables to make sure no `'static` + // lifetime promotion happens + let local_x1 = Dummy; + + let builder = sut().x1(&local_x1); + + let &Dummy = assert_send(builder).await; + } + } + + mod test_method { + use super::*; + + #[tokio::test] + async fn basic() { + struct Calculator; + + #[bon] + impl Calculator { + #[builder] + #[builder(derive(IntoFuture(Box)))] + async fn multiply(a: u32, b: u32) -> u32 { + ready(a * b).await + } + } + + // Test using IntoFuture on impl method. + let builder = Calculator::multiply().a(6).b(7); + assert_eq!(assert_send(builder).await, 42); + } + + #[tokio::test] + async fn non_send() { + struct Sut; + + #[bon] + impl Sut { + #[builder(derive(IntoFuture(Box, ?Send)))] + #[expect(clippy::future_not_send)] + async fn sut(self, value: u32) -> u32 { + non_send_future().await; + + // This future can be !Send. + ready(value * 2).await + } + } + + // Test with non-Send future. + let result = Sut.sut().value(21).await; + assert_eq!(result, 42); + } + + #[tokio::test] + async fn references_in_params() { + struct Dummy; + + #[bon] + impl Dummy { + #[builder(derive(IntoFuture(Box)))] + async fn sut<'named1, 'named2>( + &self, + _x1: &Self, + _x2: &Self, + x3: &'named1 Self, + x4: &'named2 Self, + ) -> &'named2 Self { + let _: &'named1 Self = x3; + ready(x4).await + } + } + + // Store the dummy struct in local variables to make sure no `'static` + // lifetime promotion happens + let local_self = Dummy; + let local_x1 = Dummy; + let local_x2 = Dummy; + let local_x3 = Dummy; + let local_x4 = Dummy; + + let builder = local_self + .sut() + .x1(&local_x1) + .x2(&local_x2) + .x3(&local_x3) + .x4(&local_x4); + + let _: &Dummy = assert_send(builder).await; + } + + #[tokio::test] + async fn anon_lifetime_in_return_type() { + struct Dummy; + + #[bon] + impl Dummy { + #[builder(derive(IntoFuture(Box)))] + async fn sut(&self, _x1: &Self) -> &Self { + ready(self).await + } + } + + // Store the dummy struct in local variables to make sure no `'static` + // lifetime promotion happens + let local_self = Dummy; + let local_x1 = Dummy; + + let builder = local_self.sut().x1(&local_x1); + + let _: &Dummy = assert_send(builder).await; + } + } +} diff --git a/bon/tests/integration/builder/mod.rs b/bon/tests/integration/builder/mod.rs index 3a365a29..1dd46038 100644 --- a/bon/tests/integration/builder/mod.rs +++ b/bon/tests/integration/builder/mod.rs @@ -7,6 +7,7 @@ mod attr_derive; mod attr_field; mod attr_getter; mod attr_into; +mod attr_into_future; mod attr_on; mod attr_overwritable; mod attr_required; diff --git a/bon/tests/integration/ui/compile_fail/attr_into_future.rs b/bon/tests/integration/ui/compile_fail/attr_into_future.rs new file mode 100644 index 00000000..192a49c0 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_into_future.rs @@ -0,0 +1,46 @@ +use bon::{builder, Builder}; + +// IntoFuture can only be used with async functions +#[builder(derive(IntoFuture(Box)))] +fn sync_function() -> u32 { + 42 +} + +// IntoFuture is not supported for unsafe functions +#[builder(derive(IntoFuture(Box)))] +async unsafe fn unsafe_async_function() -> u32 { + 42 +} + +// IntoFuture is incompatible with finish_fn members +#[builder(derive(IntoFuture(Box)))] +async fn with_finish_fn(#[builder(finish_fn)] value: u32) -> u32 { + value +} + +// IntoFuture requires Box argument +#[builder(derive(IntoFuture))] +async fn missing_box_arg() -> u32 { + 42 +} + +// Only Box is supported as future container +#[builder(derive(IntoFuture(Arc)))] +async fn wrong_container() -> u32 { + 42 +} + +// Wrong syntax for ?Send +#[builder(derive(IntoFuture(Box, Send)))] +async fn wrong_send_syntax() -> u32 { + 42 +} + +// Cannot be used on structs +#[derive(Builder)] +#[builder(derive(IntoFuture(Box)))] +struct AsyncConfig { + value: u32, +} + +fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/attr_into_future.stderr b/bon/tests/integration/ui/compile_fail/attr_into_future.stderr new file mode 100644 index 00000000..a9593e2b --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/attr_into_future.stderr @@ -0,0 +1,43 @@ +error: `#[builder(derive(IntoFuture(...)))` can only be used with async functions; using it with a synchronous function is likely a mistake + --> tests/integration/ui/compile_fail/attr_into_future.rs:4:1 + | +4 | #[builder(derive(IntoFuture(Box)))] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `builder` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `#[builder(derive(IntoFuture(...)))` is not supported for unsafe functions because `IntoFuture::into_future()` method is a safe method + --> tests/integration/ui/compile_fail/attr_into_future.rs:11:7 + | +11 | async unsafe fn unsafe_async_function() -> u32 { + | ^^^^^^ + +error: `#[builder(derive(IntoFuture(...)))` is incompatible with `#[builder(finish_fn)]` members because `IntoFuture::into_future()` method accepts zero parameters + --> tests/integration/ui/compile_fail/attr_into_future.rs:17:35 + | +17 | async fn with_finish_fn(#[builder(finish_fn)] value: u32) -> u32 { + | ^^^^^^^^^ + +error: expected an attribute of form `IntoFuture(Box, ...)` + --> tests/integration/ui/compile_fail/attr_into_future.rs:22:18 + | +22 | #[builder(derive(IntoFuture))] + | ^^^^^^^^^^ + +error: expected `Box` as the first argument, only boxed futures are supported + --> tests/integration/ui/compile_fail/attr_into_future.rs:28:29 + | +28 | #[builder(derive(IntoFuture(Arc)))] + | ^^^ + +error: expected `?Send` as the second argument + --> tests/integration/ui/compile_fail/attr_into_future.rs:34:34 + | +34 | #[builder(derive(IntoFuture(Box, Send)))] + | ^^^^ + +error: `#[builder(derive(IntoFuture(...)))` can only be used with async functions; using it with a synchronous function is likely a mistake + --> tests/integration/ui/compile_fail/attr_into_future.rs:42:8 + | +42 | struct AsyncConfig { + | ^^^^^^^^^^^ diff --git a/bon/tests/integration/ui/compile_fail/std_or_alloc/attr_into_future.rs b/bon/tests/integration/ui/compile_fail/std_or_alloc/attr_into_future.rs new file mode 100644 index 00000000..61f4a868 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/std_or_alloc/attr_into_future.rs @@ -0,0 +1,27 @@ +use bon::{bon, builder}; +use core::future::IntoFuture; + +fn _non_send() { + struct Sut; + + fn assert_send(_: &dyn Send) {} + + #[bon] + impl Sut { + #[builder(derive(IntoFuture(Box, ?Send)))] + async fn sut(&self, value: u32) -> u32 { + value * 2 + } + } + + assert_send(&Sut.sut().value(21).into_future()); + + #[builder(derive(IntoFuture(Box, ?Send)))] + async fn sut(value: u32) -> u32 { + value * 2 + } + + assert_send(&sut().value(21).into_future()); +} + +fn main() {} diff --git a/bon/tests/integration/ui/compile_fail/std_or_alloc/attr_into_future.stderr b/bon/tests/integration/ui/compile_fail/std_or_alloc/attr_into_future.stderr new file mode 100644 index 00000000..b8659383 --- /dev/null +++ b/bon/tests/integration/ui/compile_fail/std_or_alloc/attr_into_future.stderr @@ -0,0 +1,39 @@ +error[E0277]: `dyn Future` cannot be sent between threads safely + --> tests/integration/ui/compile_fail/std_or_alloc/attr_into_future.rs:17:17 + | +17 | assert_send(&Sut.sut().value(21).into_future()); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `dyn Future` cannot be sent between threads safely + | + = help: the trait `Send` is not implemented for `dyn Future` + = note: required for `Unique>` to implement `Send` +note: required because it appears within the type `Box>` + --> $RUST/alloc/src/boxed.rs + | + | pub struct Box< + | ^^^ +note: required because it appears within the type `Pin>>` + --> $RUST/core/src/pin.rs + | + | pub struct Pin { + | ^^^ + = note: required for the cast from `&Pin>>` to `&dyn Send` + +error[E0277]: `(dyn Future + 'static)` cannot be sent between threads safely + --> tests/integration/ui/compile_fail/std_or_alloc/attr_into_future.rs:24:17 + | +24 | assert_send(&sut().value(21).into_future()); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `(dyn Future + 'static)` cannot be sent between threads safely + | + = help: the trait `Send` is not implemented for `(dyn Future + 'static)` + = note: required for `Unique<(dyn Future + 'static)>` to implement `Send` +note: required because it appears within the type `Box<(dyn Future + 'static)>` + --> $RUST/alloc/src/boxed.rs + | + | pub struct Box< + | ^^^ +note: required because it appears within the type `Pin + 'static)>>` + --> $RUST/core/src/pin.rs + | + | pub struct Pin { + | ^^^ + = note: required for the cast from `&Pin + 'static)>>` to `&dyn Send` diff --git a/bon/tests/integration/ui/mod.rs b/bon/tests/integration/ui/mod.rs index e5c3878d..9ecee3fc 100644 --- a/bon/tests/integration/ui/mod.rs +++ b/bon/tests/integration/ui/mod.rs @@ -3,4 +3,9 @@ fn ui() { let t = trybuild::TestCases::new(); t.compile_fail("tests/integration/ui/compile_fail/*.rs"); + + if cfg!(any(feature = "std", feature = "alloc")) { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/integration/ui/compile_fail/std_or_alloc/*.rs"); + } } diff --git a/website/doctests/Cargo.toml b/website/doctests/Cargo.toml index d33474ef..08569cff 100644 --- a/website/doctests/Cargo.toml +++ b/website/doctests/Cargo.toml @@ -16,6 +16,7 @@ anyhow = "1.0" bon = { path = "../../bon", features = ["experimental-overwritable", "implied-bounds"] } buildstructor = "0.6" macro_rules_attribute = "0.2" +tokio = { version = "1.47", features = ["macros", "rt-multi-thread"] } typed-builder = "0.21" [build-dependencies] diff --git a/website/src/reference/builder/top-level/derive.md b/website/src/reference/builder/top-level/derive.md index b9fef91e..3bcd25f4 100644 --- a/website/src/reference/builder/top-level/derive.md +++ b/website/src/reference/builder/top-level/derive.md @@ -6,7 +6,7 @@ _⚠️ Do not confuse this with `#[derive(bon::Builder)]`⚠️_ Generates additional derives for the builder struct itself. The syntax is similar to the regular `#[derive(...)]` attribute, but it must be wrapped in `#[builder(derive(...))]`. Expects one or more of the supported derives separated by a comma. -The following derives are supported: [`Clone`, `Debug`](#clone-and-debug-derives), [`Into`](#into-derive). +The following derives are supported: [`Clone`, `Debug`](#clone-and-debug-derives), [`Into`](#into-derive), [`IntoFuture`](#intofuture-derive). ::: warning The format of the `Debug` output of the builder is not stable, and it may change between patch versions of `bon`. @@ -245,7 +245,7 @@ impl From> for Example { Note that `#[builder(derive(Into))]` is quite limited. Here are some things it doesn't support: -- `async` functions, because `From::from()` is synchronous +- `async` functions, because `From::from()` is synchronous. Refer to [`IntoFuture`](#intofuture-derive) instead. - `unsafe` functions, because `From::from()` is safe - Members marked with [`#[builder(finish_fn)]`](../member/finish_fn) because `From::from()` doesn't accept arguments @@ -270,3 +270,30 @@ take_example( .x1(99) ) ``` + +## `IntoFuture` Derive + +Implements [`IntoFuture`](https://doc.rust-lang.org/std/future/trait.IntoFuture.html) for the builder, allowing it to be +`await`-ed directly. To have the derived implementation produce non-`Send` futures, add `?Send` like so: `#[builder(derive(IntoFuture(Box, ?Send)))]`. + +```rust +use bon::builder; + +#[builder(derive(IntoFuture(Box)))] +async fn fetch_string(url: &str, body: Option>) -> String { + // … + "Server response".to_owned() +} + +#[tokio::main] +async fn main() { + let response = fetch_string().url("https://example.org").await; + assert_eq!(response, "Server response"); +} +``` + +Take into account that `IntoFuture` trait became stable in Rust `1.64`, which is important if you care about your MSRV. + +### Lifetimes caveat + +There is a caveat that `dyn Trait` objects can only have a single `+ 'lifetime` bound which is the Rust language's fundamental limitation. So the generated `IntoFuture` implementation squashes all lifetimes into a single `'builder` lifetime. This means it's not strictly equivalent the default `finish_fn` in terms of lifetimes. This should generally not be a problem unless the output type of the function's `Future` contains more than one lifetime.