diff --git a/.travis.yml b/.travis.yml index 31926926..268fd9e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ matrix: - rust: 1.15.0 env: JOB=build CARGO_FEATURES="struct_default" - rust: nightly - env: JOB=build CARGO_FEATURES="compiletests logging" + env: JOB=build CARGO_FEATURES="nightlytests logging" - rust: nightly env: JOB=style_check allow_failures: diff --git a/derive_builder/CHANGELOG.md b/derive_builder/CHANGELOG.md index ae845b7f..a66c62d5 100644 --- a/derive_builder/CHANGELOG.md +++ b/derive_builder/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## Unreleased + +### Added +- try_setters, e.g. `#[builder(try_setter)]`. These setters are exposed alongside the + normal field setters and allow callers to pass in values which have + fallible conversions to the needed type through `TryInto`. This attribute + can only be used on nightly when until `#![feature(try_from)]` is declared + in the consuming crate's root; this will change when Rust issue + [#33417](https://github.com/rust-lang/rust/issues/33417) is resolved. + ## [0.4.3] - 2017-04-11 ### Fixed diff --git a/derive_builder/src/lib.rs b/derive_builder/src/lib.rs index 854c5e3e..3a6e85c2 100644 --- a/derive_builder/src/lib.rs +++ b/derive_builder/src/lib.rs @@ -216,6 +216,50 @@ //! } //! ``` //! +//! ## Fallible Setters +//! +//! Alongside the normal setter methods, you can expose fallible setters which are generic over +//! the `TryInto` trait. TryInto is a not-yet-stable trait +//! (see rust-lang issue [#33417](https://github.com/rust-lang/rust/issues/33417)) similar to +//! `Into` with the key distinction that the conversion can fail, and therefore produces a `Result`. +//! +//! You can only declare the `try_setter` attribute today if you're targeting nightly, and you have +//! to add `#![feature(try_from)]` to your crate to use it. +//! +//! +//! ```rust,ignore +//! #![feature(try_from)] +//! # #[macro_use] +//! # extern crate derive_builder; +//! # +//! #[derive(Builder, Debug, PartialEq)] +//! #[builder(try_setter, setter(into))] +//! struct Lorem { +//! pub name: String, +//! pub ipsum: u8, +//! } +//! +//! #[derive(Builder, Debug, PartialEq)] +//! struct Ipsum { +//! #[builder(try_setter, setter(into, name = "foo"))] +//! pub dolor: u8, +//! } +//! +//! fn main() { +//! LoremBuilder::default() +//! .try_ipsum(1u16).unwrap() +//! .name("hello") +//! .build() +//! .expect("1 fits into a u8"); +//! +//! IpsumBuilder::default() +//! .try_foo(1u16) +//! .unwrap() +//! .build() +//! .expect("1 fits into a u8"); +//! } +//! ``` +//! //! ## Default Values //! //! You can define default values for each field via annotation by `#[builder(default="...")]`, @@ -366,6 +410,9 @@ //! - If derive_builder depends on your crate, and vice versa, then a cyclic //! dependency would occur. To break it you could try to depend on the //! [`derive_builder_core`] crate instead. +//! - The `try_setter` attribute and `owned` builder pattern are not compatible in practice; +//! an error during building will consume the builder, making it impossible to continue +//! construction. //! //! ## Debugging Info //! diff --git a/derive_builder/src/options/field_mode.rs b/derive_builder/src/options/field_mode.rs index 06159ed1..cea8a321 100644 --- a/derive_builder/src/options/field_mode.rs +++ b/derive_builder/src/options/field_mode.rs @@ -81,6 +81,7 @@ impl OptionsBuilder { setter_vis: f!(setter_vis), default_expression: f!(default_expression), setter_into: f!(setter_into), + try_setter: f!(try_setter), no_std: f!(no_std), mode: mode, } @@ -133,6 +134,7 @@ impl From> for FieldOptions { field_ident: field_ident, field_type: field_type, setter_into: b.setter_into.unwrap_or(false), + try_setter: b.try_setter.unwrap_or(false), deprecation_notes: b.mode.deprecation_notes.clone(), default_expression: b.default_expression.clone(), use_default_struct: b.mode.use_default_struct, diff --git a/derive_builder/src/options/field_options.rs b/derive_builder/src/options/field_options.rs index a17d1977..0af51db7 100644 --- a/derive_builder/src/options/field_options.rs +++ b/derive_builder/src/options/field_options.rs @@ -31,6 +31,8 @@ pub struct FieldOptions { pub attrs: Vec, /// Bindings to libstd or libcore. pub bindings: Bindings, + /// Enables code generation for the TryInto setter. + pub try_setter: bool, } impl DefaultExpression { @@ -58,6 +60,7 @@ impl FieldOptions { pub fn as_setter<'a>(&'a self) -> Setter<'a> { Setter { enabled: self.setter_enabled, + try_setter: self.try_setter, visibility: &self.setter_visibility, pattern: self.builder_pattern, attrs: &self.attrs, diff --git a/derive_builder/src/options/mod.rs b/derive_builder/src/options/mod.rs index e2b43a6c..477270be 100644 --- a/derive_builder/src/options/mod.rs +++ b/derive_builder/src/options/mod.rs @@ -45,6 +45,7 @@ pub struct OptionsBuilder { setter_vis: Option, default_expression: Option, setter_into: Option, + try_setter: Option, no_std: Option, mode: Mode, } @@ -66,6 +67,7 @@ impl From for OptionsBuilder { setter_prefix: None, setter_name: None, setter_vis: None, + try_setter: None, default_expression: None, setter_into: None, no_std: None, @@ -100,6 +102,12 @@ impl OptionsBuilder where desc: "setter type conversion", map: |x: bool| { x }, } + + impl_setter!{ + ident: try_setter, + desc: "try_setter activation", + map: |x: bool| { x }, + } impl_setter!{ ident: default_expression, @@ -198,6 +206,9 @@ impl OptionsBuilder where // setter implicitly enabled self.setter_enabled(true) }, + "try_setter" => { + self.try_setter(true) + } "default" => { if !cfg!(feature = "struct_default") && self.mode.struct_mode() { let where_info = self.where_diagnostics(); diff --git a/derive_builder/src/options/struct_mode.rs b/derive_builder/src/options/struct_mode.rs index a43e7d49..bf843b65 100644 --- a/derive_builder/src/options/struct_mode.rs +++ b/derive_builder/src/options/struct_mode.rs @@ -78,6 +78,7 @@ impl From> for (StructOptions, OptionsBuilder RawTokens<&'static str> { + RawTokens(if self.no_std { + ":: core :: convert :: TryInto" + } else { + ":: std :: convert :: TryInto" + }) + } } #[test] diff --git a/derive_builder_core/src/lib.rs b/derive_builder_core/src/lib.rs index e97873e0..9518ada5 100644 --- a/derive_builder_core/src/lib.rs +++ b/derive_builder_core/src/lib.rs @@ -22,6 +22,7 @@ //! [`derive_builder_core`]: https://!crates.io/crates/derive_builder_core #![deny(warnings, missing_docs)] +#![cfg_attr(test, recursion_limit = "100")] extern crate proc_macro; extern crate syn; diff --git a/derive_builder_core/src/setter.rs b/derive_builder_core/src/setter.rs index cfcc8908..1841c1c7 100644 --- a/derive_builder_core/src/setter.rs +++ b/derive_builder_core/src/setter.rs @@ -34,6 +34,8 @@ use Bindings; pub struct Setter<'a> { /// Enables code generation for this setter fn. pub enabled: bool, + /// Enables code generation for the `try_` variant of this setter fn. + pub try_setter: bool, /// Visibility of the setter, e.g. `syn::Visibility::Public`. pub visibility: &'a syn::Visibility, /// How the setter method takes and returns `self` (e.g. mutably). @@ -117,6 +119,26 @@ impl<'a> ToTokens for Setter<'a> { new.#field_ident = #option::Some(#into_value); new })); + + if self.try_setter { + let try_into = self.bindings.try_into_trait(); + let try_ty_params = quote!(>); + let try_ident = syn::Ident::new(format!("try_{}", ident)); + let result = self.bindings.result_ty(); + + tokens.append(quote!( + #(#attrs)* + #vis fn #try_ident #try_ty_params (#self_param, value: VALUE) + -> #result<#return_ty, VALUE::Error> + { + let converted : #ty = value.try_into()?; + let mut new = #self_into_return_ty; + new.#field_ident = #option::Some(converted); + Ok(new) + })); + } else { + trace!("Skipping try_setter for `{}`.", self.field_ident); + } } else { trace!("Skipping setter for `{}`.", self.field_ident); } @@ -131,6 +153,7 @@ macro_rules! default_setter { () => { Setter { enabled: true, + try_setter: false, visibility: &syn::Visibility::Public, pattern: BuilderPattern::Mutable, attrs: &vec![], @@ -221,7 +244,7 @@ mod tests { )); } - // including + // including try_setter #[test] fn full() { let attrs = vec![syn::parse_outer_attr("#[some_attr]").unwrap()]; @@ -233,6 +256,7 @@ mod tests { setter.attrs = attrs.as_slice(); setter.generic_into = true; setter.deprecation_notes = &deprecated; + setter.try_setter = true; assert_eq!(quote!(#setter), quote!( #[some_attr] @@ -242,6 +266,15 @@ mod tests { new.foo = ::std::option::Option::Some(value.into()); new } + + #[some_attr] + pub fn try_foo>(&mut self, value: VALUE) + -> ::std::result::Result<&mut Self, VALUE::Error> { + let converted : Foo = value.try_into()?; + let mut new = self; + new.foo = ::std::option::Option::Some(converted); + Ok(new) + } )); } @@ -282,4 +315,27 @@ mod tests { assert_eq!(quote!(#setter), quote!()); } + + #[test] + fn try_setter() { + let mut setter : Setter = default_setter!(); + setter.pattern = BuilderPattern::Mutable; + setter.try_setter = true; + + assert_eq!(quote!(#setter), quote!( + pub fn foo(&mut self, value: Foo) -> &mut Self { + let mut new = self; + new.foo = ::std::option::Option::Some(value); + new + } + + pub fn try_foo>(&mut self, value: VALUE) + -> ::std::result::Result<&mut Self, VALUE::Error> { + let converted : Foo = value.try_into()?; + let mut new = self; + new.foo = ::std::option::Option::Some(converted); + Ok(new) + } + )); + } } diff --git a/derive_builder_test/Cargo.toml b/derive_builder_test/Cargo.toml index d93b78b1..a687f151 100644 --- a/derive_builder_test/Cargo.toml +++ b/derive_builder_test/Cargo.toml @@ -9,7 +9,7 @@ publish = false path = "src/lib.rs" [features] -compiletests = ["compiletest_rs"] +nightlytests = ["compiletest_rs"] logging = [ "derive_builder/logging" ] struct_default = [ "derive_builder/struct_default" ] diff --git a/derive_builder_test/tests/compiletests.rs b/derive_builder_test/tests/compiletests.rs index 23c07b02..022793cd 100644 --- a/derive_builder_test/tests/compiletests.rs +++ b/derive_builder_test/tests/compiletests.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "compiletests")] +#![cfg(feature = "nightlytests")] extern crate compiletest_rs as compiletest; // note: diff --git a/derive_builder_test/tests/try_setter.rs b/derive_builder_test/tests/try_setter.rs new file mode 100644 index 00000000..49d28086 --- /dev/null +++ b/derive_builder_test/tests/try_setter.rs @@ -0,0 +1,97 @@ +#![cfg_attr(feature = "nightlytests", feature(try_from))] + +#[macro_use] +extern crate derive_builder; + +#[cfg(feature = "nightlytests")] +#[allow(unused_imports)] +mod struct_level { + #[cfg(feature = "nightlytests")] + use std::convert::TryFrom; + + use std::net::{IpAddr, AddrParseError}; + use std::str::FromStr; + use std::string::ToString; + + #[derive(Debug, Clone, PartialEq)] + pub struct MyAddr(IpAddr); + + impl From for MyAddr { + fn from(v: IpAddr) -> Self { + MyAddr(v) + } + } + + #[cfg(feature = "nightlytests")] + impl<'a> TryFrom<&'a str> for MyAddr { + type Error = AddrParseError; + + fn try_from(v: &str) -> Result { + Ok(MyAddr(v.parse()?)) + } + } + + #[derive(Debug, PartialEq, Builder)] + #[builder(try_setter, setter(into))] + struct Lorem { + pub source: MyAddr, + pub dest: MyAddr, + } + + #[derive(Debug, PartialEq, Builder)] + #[builder(try_setter, setter(into, prefix = "set"))] + struct Ipsum { + pub source: MyAddr + } + + fn exact_helper() -> Result { + LoremBuilder::default() + .source(IpAddr::from_str("1.2.3.4").unwrap()) + .dest(IpAddr::from_str("0.0.0.0").unwrap()) + .build() + } + + #[cfg(feature = "nightlytests")] + fn try_helper() -> Result { + LoremBuilder::default() + .try_source("1.2.3.4").map_err(|e| e.to_string())? + .try_dest("0.0.0.0").map_err(|e| e.to_string())? + .build() + } + + #[test] + fn infallible_set() { + let _ = LoremBuilder::default() + .source(IpAddr::from_str("1.2.3.4").unwrap()) + .dest(IpAddr::from_str("0.0.0.0").unwrap()) + .build(); + } + + #[test] + #[cfg(feature = "nightlytests")] + fn fallible_set() { + let mut builder = LoremBuilder::default(); + let try_result = builder.try_source("1.2.3.4"); + let built = try_result.expect("Passed well-formed address") + .dest(IpAddr::from_str("0.0.0.0").unwrap()) + .build() + .unwrap(); + assert_eq!(built, exact_helper().unwrap()); + } + + #[test] + #[cfg(feature = "nightlytests")] + fn with_helper() { + assert_eq!(exact_helper().unwrap(), try_helper().unwrap()); + } + + #[test] + #[cfg(feature = "nightlytests")] + fn renamed() { + IpsumBuilder::default() + .try_set_source("0.0.0.0") + .unwrap() + .build() + .expect("All fields were provided"); + } +} \ No newline at end of file