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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions