colin-kiegel / rust-derive-builder

derive builder implementation for rust structs
https://colin-kiegel.github.io/rust-derive-builder/
Apache License 2.0
1.32k stars 88 forks source link

feat(into_builder): implement From trait for builder #215

Closed jonas-x closed 2 years ago

jonas-x commented 3 years ago

Implement From<Foo> for FooBuilder to allow creating a builder from an instance. This is useful when you want to create a new, modified, instance from an existing one.

Issue: https://github.com/colin-kiegel/rust-derive-builder/issues/170

TedDriggs commented 2 years ago

After 7 months' intermittent consideration, I don't see a way for this to be predictable for all permutations of builder options. Skipped fields and custom setters are the two most immediate places of danger, but I suspect custom build, validation, and defaulting functions would also produce surprising behavior in as-yet-unexplored edge cases.

Happily, derive_builder doesn't need native support for this: It's easy to write a custom From impl where the compiler will help make sure all fields are accounted for in the conversion.

impl From<Foo> for FooBuilder {
    fn from(v: Foo) -> Self {
        let Foo {
            first,
            second
        } = v;

        // OPTION A: Use setters
        let mut builder = FooBuilder::default();
        builder.first(first);
        builder.second(second);

        builder

        // OPTION B: Don't use setters
        FooBuilder {
            first: Some(first),
            second: Some(second),
        }
    }
}

By destructuring Foo and avoiding the use of .., the compiler will now 1) error if any field is left behind in Foo, and 2) will warn about an unused variable if a field is extracted from Foo but not passed through to FooBuilder.

If a particular project is making heavy use of this impl From<Foo> for FooBuilder pattern, it could define a crate-private hygienic macro to further reduce boilerplate. For example:

macro_rules! into_builder {
    ($builder:ty : $base:ident => $($field:ident),* ) => {
        impl From<$base> for $builder {
            fn from(__base: $base) -> Self {
                let $base {
                    $($field),*
                } = __base;

                Self {
                    $($field: Some($field)),*
                }
            }
        }
    }
}

into_builder!(FooBuilder : Foo => first, second);

This has the same drift protection as the more-explicit version, so adding a field to the base struct without updating the macro invocation would cause a compile error.


Given the lack of a single right behavior, the low demonstrated demand, and the availability of safe and explicit workarounds, I'm closing this and #170 as "Won't Do."