Builder pattern with generic parameters and default values

I am trying to implement the builder pattern for a struct with generic parameters. The builder struct implements the default trait returning itself with the properties set to specific implementations of the generic parameters.

The problem is that once the builder is generated with the default properties, the user should still be able to change it to other types/implementations of the generic types.

#[derive(Educe)]
#[educe(Debug)]
pub struct ArtiConnector<R: Runtime, TC: TlsConn> {
    /// Arti client to connect to Tor.
    #[educe(Debug(ignore))]
    client: Arc<TorClient<R>>,

    /// The runtime to use for blocking operations.
    rt: R,

    /// Has type `TlsConn` corresponding to any implementation of the trait [`ureq::unversioned::transport::Connector`].
    #[educe(Debug(ignore))]
    tls_conn: TC,
}

impl<R: Runtime, TC: TlsConn> std::default::Default for ArtiConnectorBuilder<R, TC> {
    fn default() -> Self {
        // Default TorClientConfig
        let config = arti_client::TorClientConfig::default();

        // Default Runtime
        let rt = tor_rtcompat::PreferredRuntime::create().expect("Failed to create runtime.");

        // Default TorClient
        let client = TorClient::with_runtime(rt.clone())
            .create_unbootstrapped()
            .expect("Error creating Tor Client.");
        client
            .reconfigure(&config, Reconfigure::AllOrNothing)
            .expect("Error applying config to Tor Client.");

        // Default TlsConn
        let tls_conn = ureq::unversioned::transport::DefaultConnector::default();
        
        Self {
            client: Arc::new(client),
            rt,
            tls_conn,
            tor_config: config,
        }
    }
}

impl ArtiConnector<tor_rtcompat::PreferredRuntime, ureq::unversioned::transport::DefaultConnector> {
    pub fn builder() -> ArtiConnectorBuilder<tor_rtcompat::PreferredRuntime, ureq::unversioned::transport::DefaultConnector> {
        ArtiConnectorBuilder::default()
    }
}

pub struct ArtiConnectorBuilder<R: Runtime = tor_rtcompat::PreferredRuntime, TC: TlsConn = ureq::unversioned::transport::DefaultConnector> {
   client: Arc<TorClient<R>>,
   rt: R,
   tls_conn: TC,
   tor_config: TorClientConfig,
}

impl <R: Runtime, TC: TlsConn> ArtiConnectorBuilder<R, TC> {
    pub fn build(self) -> ArtiConnector<R, TC> {
        self.client.reconfigure(&self.tor_config, Reconfigure::AllOrNothing)
            .expect("Error applying config to Tor Client.");
        
        ArtiConnector {
            client: self.client,
            rt: self.rt,
            tls_conn: self.tls_conn,
        }
    }

    pub fn with_tor_client(mut self, client: TorClient<R>) -> Self {
        self.client = client.into();
        self
    }

    pub fn with_tor_config(mut self, config: TorClientConfig) -> Self {
        self.tor_config = config;
        self
    }

    pub fn with_runtime(mut self, rt: R) -> Self {
        self.rt = rt;
        self
    }

    pub fn with_tls_conn(mut self, tls_conn: TC) -> Self {
        self.tls_conn = tls_conn;
        self
    }
}

Error:

error[E0308]: mismatched types
   --> crates/arti-ureq/src/lib.rs:104:30
    |
84  | impl<R: Runtime, TC: TlsConn> std::default::Default for ArtiConnectorBuilder<R,...
    |      - expected this type parameter
...
104 |             client: Arc::new(client),
    |                     -------- ^^^^^^ expected `TorClient<R>`, found `TorClient<PreferredRuntime>`
    |                     |
    |                     arguments to this function are incorrect
    |
    = note: expected struct `TorClient<R>`
               found struct `TorClient<tor_rtcompat::PreferredRuntime>`
note: associated function defined here
   --> /Users/name/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/alloc/src/sync.rs:385:12
    |
385 |     pub fn new(data: T) -> Arc<T> {
    |            ^^^

error[E0308]: mismatched types
   --> crates/arti-ureq/src/lib.rs:105:13
    |
84  | impl<R: Runtime, TC: TlsConn> std::default::Default for ArtiConnectorBuilder<R,...
    |      - expected this type parameter
...
105 |             rt,
    |             ^^ expected type parameter `R`, found `PreferredRuntime`
    |
    = note: expected type parameter `R`
                       found struct `tor_rtcompat::PreferredRuntime`

error[E0308]: mismatched types
   --> crates/arti-ureq/src/lib.rs:106:13
    |
84  | impl<R: Runtime, TC: TlsConn> std::default::Default for ArtiConnectorBuilder<R,...
    |                  -- expected this type parameter
...
106 |             tls_conn,
    |             ^^^^^^^^ expected type parameter `TC`, found `DefaultConnector`
    |
    = note: expected type parameter `TC`
                       found struct `DefaultConnector`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `arti-ureq` (lib) due to 3 previous errors

How would I fix this? I want the default method in the builder to return specific implementations of the generic parameters, but the user should be able to edit those properties to other types implementing the trait of the generic parameters.

  1. You must implement Default only for the types you want to use in the default value, not for all <R, TC>.
  2. The builder methods must change the type, avoiding the use of Self and assignment. Whenever you return Self, you are saying that the type parameters are not changing.
impl Default for ArtiConnectorBuilder<tor_rtcompat::PreferredRuntime, ureq::unversioned::transport::DefaultConnector> {
    ...
}

impl<R: Runtime, TC: TlsConn> ArtiConnectorBuilder<R, TC> {
    pub fn with_runtime<R2: Runtime>(self, rt: R2) -> ArtiConnectorBuilder<R2, TC> {
        ArtiConnectorBuilder {
            client: self.client,
            rt: rt,
            tls_conn: self.tls_conn,
        }
    }
}
1 Like

I updated my code to this:

#[derive(Educe)]
#[educe(Debug)]
pub struct ArtiConnector<R: Runtime, TC: TlsConn> {
    /// Arti client to connect to Tor.
    #[educe(Debug(ignore))]
    client: Arc<TorClient<R>>,

    /// The runtime to use for blocking operations.
    rt: R,

    /// Has type `TlsConn` corresponding to any implementation of the trait [`ureq::unversioned::transport::Connector`].
    #[educe(Debug(ignore))]
    tls_conn: TC,
}

impl std::default::Default for ArtiConnectorBuilder<tor_rtcompat::PreferredRuntime, ureq::unversioned::transport::DefaultConnector> {
    fn default() -> Self {
        // Default TorClientConfig
        let config = arti_client::TorClientConfig::default();

        // Default Runtime
        let rt = tor_rtcompat::PreferredRuntime::create().expect("Failed to create runtime.");

        // Default TorClient
        let client = TorClient::with_runtime(rt.clone())
            .create_unbootstrapped()
            .expect("Error creating Tor Client.");
        client
            .reconfigure(&config, Reconfigure::AllOrNothing)
            .expect("Error applying config to Tor Client.");

        // Default TlsConn
        let tls_conn = ureq::unversioned::transport::DefaultConnector::default();
        
        Self {
            client: Arc::new(client),
            rt,
            tls_conn,
            tor_config: config,
        }
    }
}

impl ArtiConnector<tor_rtcompat::PreferredRuntime, ureq::unversioned::transport::DefaultConnector> {
    pub fn builder() -> ArtiConnectorBuilder<tor_rtcompat::PreferredRuntime, ureq::unversioned::transport::DefaultConnector> {
        ArtiConnectorBuilder::default()
    }
}

pub struct ArtiConnectorBuilder<R: Runtime, TC: TlsConn> {
   client: Arc<TorClient<R>>,
   rt: R,
   tls_conn: TC,
   tor_config: TorClientConfig,
}

impl <R: Runtime, TC: TlsConn> ArtiConnectorBuilder<R, TC> {
    pub fn build(self) -> ArtiConnector<R, TC> {
        self.client.reconfigure(&self.tor_config, Reconfigure::AllOrNothing)
            .expect("Error applying config to Tor Client.");
        
        ArtiConnector {
            client: self.client,
            rt: self.rt,
            tls_conn: self.tls_conn,
        }
    }

    pub fn with_tor_client(mut self, client: TorClient<R>) -> Self {
        self.client = client.into();
        self
    }

    pub fn with_tor_config(mut self, config: TorClientConfig) -> Self {
        self.tor_config = config;
        self
    }

    pub fn with_runtime<R2: Runtime>(self, rt: R2) -> ArtiConnectorBuilder<R2, TC> {
        ArtiConnectorBuilder {
            client: self.client,
            rt: rt,
            tls_conn: self.tls_conn,
            tor_config: self.tor_config,
        }
    }

    pub fn with_tls_conn(mut self, tls_conn: TC) -> Self {
        self.tls_conn = tls_conn;
        self
    }
}

(changed)

    pub fn with_runtime<R2: Runtime>(self, rt: R2) -> ArtiConnectorBuilder<R2, TC> {
        ArtiConnectorBuilder {
            client: self.client,
            rt: rt,
            tls_conn: self.tls_conn,
            tor_config: self.tor_config,
        }
    }

It generates this error:

error[E0308]: mismatched types
   --> crates/arti-ureq/src/lib.rs:149:21
    |
125 | impl <R: Runtime, TC: TlsConn> ArtiConnectorBuilder<R, TC> {
    |       - found type parameter
...
147 |     pub fn with_runtime<R2: Runtime>(self, rt: R2) -> ArtiConnectorBuilder<R2, ...
    |                         -- expected type parameter
148 |         ArtiConnectorBuilder {
149 |             client: self.client,
    |                     ^^^^^^^^^^^ expected `Arc<TorClient<R2>>`, found `Arc<TorClient<R>>`
    |
    = note: expected struct `Arc<TorClient<R2>>`
               found struct `Arc<TorClient<R>>`
    = note: a type parameter was expected, but a different one was found; you might be missing a type parameter or trait bound
    = note: for more information, visit https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters

@kpreid I can fix it by defining client again in with_runtime.

    pub fn with_runtime<R2: Runtime>(self, rt: R2) -> ArtiConnectorBuilder<R2, TC> {
        let client = TorClient::with_runtime(rt.clone())
            .create_unbootstrapped()
            .expect("Error creating Tor Client.");
        ArtiConnectorBuilder {
            client: client.into(),
            rt: rt,
            tls_conn: self.tls_conn,
            tor_config: self.tor_config,
        }
    }

But the problem is that I am then overwriting self.client. Which might cause unexpected behavior when using with_tor_client.

You could design your builder so that using those two methods together becomes impossible (i.e. using the typestate pattern).

1 Like

Yesss.... I think I fixed it using the typestate pattern:

#[derive(Educe)]
#[educe(Debug)]
pub struct ArtiConnector<R: Runtime, TC: TlsConn> {
    /// Arti client to connect to Tor.
    #[educe(Debug(ignore))]
    client: Arc<TorClient<R>>,

    /// The runtime to use for blocking operations.
    rt: R,

    /// Has type `TlsConn` corresponding to any implementation of the trait [`ureq::unversioned::transport::Connector`].
    #[educe(Debug(ignore))]
    tls_conn: TC,
}

pub struct DefaultTorClient;
pub struct CustomTorClient;

impl std::default::Default for ArtiConnectorBuilder<tor_rtcompat::PreferredRuntime, ureq::unversioned::transport::DefaultConnector, DefaultTorClient> {
    fn default() -> Self {
        // Default TorClientConfig
        let config = arti_client::TorClientConfig::default();

        // Default Runtime
        let rt = tor_rtcompat::PreferredRuntime::create().expect("Failed to create runtime.");

        // Default TorClient
        let client = TorClient::with_runtime(rt.clone())
            .create_unbootstrapped()
            .expect("Error creating Tor Client.");
        client
            .reconfigure(&config, Reconfigure::AllOrNothing)
            .expect("Error applying config to Tor Client.");

        // Default TlsConn
        let tls_conn = ureq::unversioned::transport::DefaultConnector::default();
        
        Self {
            client: Arc::new(client),
            rt,
            tls_conn,
            tor_config: config,
            _client_state: std::marker::PhantomData,
        }
    }
}

impl ArtiConnector<tor_rtcompat::PreferredRuntime, ureq::unversioned::transport::DefaultConnector> {
    pub fn builder() -> ArtiConnectorBuilder<tor_rtcompat::PreferredRuntime, ureq::unversioned::transport::DefaultConnector, DefaultTorClient> {
        ArtiConnectorBuilder::default()
    }
}

pub struct ArtiConnectorBuilder<R: Runtime, TC: TlsConn, TorClientState> {
   client: Arc<TorClient<R>>,
   rt: R,
   tls_conn: TC,
   tor_config: TorClientConfig,
   _client_state: std::marker::PhantomData<TorClientState>,
}

impl <R: Runtime, TC: TlsConn, TorClientState> ArtiConnectorBuilder<R, TC, TorClientState> {
    pub fn build(self) -> ArtiConnector<R, TC> {
        self.client
            .reconfigure(&self.tor_config, Reconfigure::AllOrNothing)
            .expect("Error applying config to Tor Client.");

        ArtiConnector {
            client: self.client,
            rt: self.rt,
            tls_conn: self.tls_conn,
        }
    }

    pub fn with_tor_config(mut self, config: TorClientConfig) -> Self {
        self.tor_config = config;
        self
    }
    
    pub fn with_tls_conn(mut self, tls_conn: TC) -> Self {
        self.tls_conn = tls_conn;
        self
    }
}

impl<R: Runtime, TC: TlsConn> ArtiConnectorBuilder<R, TC, DefaultTorClient> {
    pub fn with_runtime<R2: Runtime>(self, rt: R2) -> ArtiConnectorBuilder<R2, TC, DefaultTorClient> {
        let client = TorClient::with_runtime(rt.clone())
            .create_unbootstrapped()
            .expect("Error creating Tor Client.");
        client
            .reconfigure(&self.tor_config, Reconfigure::AllOrNothing)
            .expect("Error applying config to Tor Client.");

        ArtiConnectorBuilder {
            client: Arc::new(client),
            rt,
            tls_conn: self.tls_conn,
            tor_config: self.tor_config,
            _client_state: std::marker::PhantomData,
        }
    }

    pub fn with_tor_client<R2: Runtime>(mut self, client: TorClient<R2>) -> ArtiConnectorBuilder<R2, TC, CustomTorClient> {
        ArtiConnectorBuilder {
            rt: client.runtime().clone(),
            client: client.into(),
            tls_conn: self.tls_conn,
            tor_config: self.tor_config,
            _client_state: std::marker::PhantomData,
        }
    }
}

Usage:

// works
let builder = arti_ureq::ArtiConnector::builder().with_runtime(rt).with_tor_client(client);

// doesn't work (as expected) => no function with_runtime found
let builder = arti_ureq::ArtiConnector::builder().with_tor_client(client).with_runtime(rt);

// works
let builder = arti_ureq::ArtiConnector::builder().with_tor_client(client);

// works
let builder = arti_ureq::ArtiConnector::builder().with_runtime(rt);