Skip to content

Reinterpret generic type #328

@xbwwj

Description

@xbwwj

Description

Allow reinterpret the generic type in builder.

For simple skip case:

#[derive(Builder)]
struct Foo<T> {
    #[builder(skip)]
    foo: Option<T>
}

impl<T1, S> FooBuilder<T1, S> {
    // change generic T1 to T2
    // here `foo` is name of the field
    fn reinterpret_foo<T2>(self) -> FooBuilder<T2, S> {
        todo!()
    }
}

For IsUnset case: reinterpet is only allowed for unset fields.

#[derive(Builder)]
struct Bar<T> {
    bar: T
}

impl<T1, S> BarBuilder<T1, S>
where
    S::Value: IsUnset
{
    fn reintepret_bar<T2>(self) -> BarBuilder<T2, S> {
        todo!()
    }
}

Implementation

The implementation is very trivial. For example, for skip case:

impl<T1, S> FooBuilder<T1, S>
{
    #[allow(deprecated)] // <- generating deprecated warnings here
    fn reinterpret_foo<T2>(self) -> FooBuilder<T2, S>
where {
        let Self {
            __unsafe_private_named,
            ..
        } = self;
        FooBuilder {
            __unsafe_private_phantom: PhantomData,
            __unsafe_private_named,
        }
    }
}

But it requires destructing the builder and fires a deprecated warning. So I think it should better be implemented in bon itself rather than user side.

A use case

This method is useful for default generic implementation and user customizable trait impl inside builder struct.

Imagine the case that we have a raw API which takes a string input and returns many fields:

fn fake_query_user_info_raw(name: &str) -> serde_json::Value {
    json!({
        "name": name,
        "age": 30,
        "phone": "123456789",
        "department": "Accounting",
        // many more fields in real world
    })
}

We want to provide a wrapper that deserialize part of the fields in advance, but still allow the user to customize deserialization for more fields, using the #[serde(flatten] feature.

#[derive(Deserialize, Debug)]
struct Message<Extra = ()> {
    // provided by our wrapper
    name: String,
    age: u32,

    // user customizable
    #[serde(flatten)]
    extra: Extra,
}

We want to provide wrapper API like below.

// The simple API call, no need to type generic
let simple = query_user_info().name("Alice").fetch();
dbg!(simple);

// Advanced users can provide custom deserialize
let advanced = query_user_info().name("Alice").extra::<PhoneExtra>().fetch();
dbg!(advanced);

#[derive(Deserialize, Debug)]
struct PhoneExtra {
    phone: String,
}

To craft such an API, we define the parameters with bon.

/// Arguments to the API.
#[derive(Builder)]
pub struct Args<Extra = ()> {
    #[builder(into)]
    name: String,

    #[builder(skip)]
    _extra: PhantomData<Extra>,
}

and provide custom builder methods:

impl<Extra, S> ArgsBuilder<Extra, S> {
    fn extra<NewExtra>(self) -> ArgsBuilder<NewExtra, S>
where {
        self.reinterpret_extra::<NewExtra>()
    }
}

then wrappers:

fn query_user_info() -> ArgsBuilder {
    Args::builder()
}

impl<Extra, S> ArgsBuilder<Extra, S>
where
    S: args_builder::IsComplete,
{
    fn fetch(self) -> Message<Extra>
    where
        Extra: DeserializeOwned,
    {
        let args = self.build();
        let raw = fake_query_user_info_raw(&args.name);
        serde_json::from_value(raw).unwrap()
    }
}

Why prefer such API?

As Rust does not allow default generic in function, users have to bind the generic in front,

let simple = query_user_info::<()>.name("Alice").fetch();

This syntax poses more cognitive and typing burden for lightweight users, compared to the former one.

More to discussion

  • the syntax: automatically generated for generic field, or requires explicit #[builder(reinterpret)]
  • reinterpret bound: #[builder(reinterpret(bound = T)]

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions