Best practices features and dependencies in a crate

I am making a custom crate using ureq. ureq has a connector for both rustls (ureq::unversioned::transport::RustlsConnector) and native-tls (ureq::unversioned::transport::NativeTlsConnector).

For either, you have to enable the corresponding feature flag. This is how I currently do it in the Cargo.toml of my lib.

ureq = { version = "=3.0.5", features = [ "rustls", "native-tls" ] }

The user can pass an argument of type ureq::tls::TlsProvider (with values Rustls and NativeTls) to my library to select what type of connector to use. Currently the connector is determined like this:

if tls_provider == TlsProvider::NativeTls {
    let connector_native_tls =
        self.chain(ureq::unversioned::transport::NativeTlsConnector::default());
    return connector_native_tls;
}

let connector_rustls =
    self.chain(ureq::unversioned::transport::RustlsConnector::default());
return connector_rustls;

But a better way would be to do

self
.chain(ureq::unversioned::transport::NativeTlsConnector::default())
.chain(ureq::unversioned::transport::RustlsConnector::default());

and depending on the TlsProvider passed to ureq::Agent the correct connector in the chain will be used.

So my first question is: currently the Cargo.toml of my crate enables both the rustls and native-tls feature, because we want to support both for the user. But this means probably one tls implementation is compiled into the project using my crate without it ever being used. Is there a better way to do this? The user also has to include ureq in his own Cargo.toml, maybe I could remove the feature flags in my own Cargo.toml and derive it from the features the user enables?

My second question: Currently I pinned the version of ureq in my library to 3.0.5. This means the user can only use that exact version for his own project? Is there a way to support more ureq versions (those compatible with my code)?

you can use separate feature flags:

# Cargo.toml
[dependencies]
ureq = { version = "xxx", default-features = false }

[features]
default = [ "rustls" ]
rustls = [ "ureq/rustls" ]
native-tls = [ "ureq/native-tls" ]

no. currently, cargo allows the user to have a different version as dependency. however, it will cause the build to fail if they pass an argument of a type from the wrong version.

if you are using types from a dependency crate as part of your public API, you can re-export the dependency from your crate, something like this:

// lib.rs

// option 1: re-export the crate as whole
pub extern crate ureq;

// option 2: selectively re-export the required items
pub use ureq::transport::*;

this way, the user of your library don't need to add the transitive dependency directly.

a lint will fire when a version mismatch is detected, if you enable the public-dependency feature.

as long ureq doesn't break semver compatibility guarantee, you don't need to lockdown a specific version, you can just specify the major version (and minor version too if you want to be conservative), and cargo will try to unify the dependencies based on semver.

2 Likes

Thanks. I updated the Cargo.toml of my crate to

ureq = { version = "=3.0.5", default-features = false }

[features]
default = ["rustls"]
rustls = ["ureq/rustls"]
native-tls = ["ureq/native-tls"]

And made this method:

    /// Returns connector chain depending on features flag.
    #[allow(unreachable_code)]
    fn connector_chain(self) -> impl UreqConnector {
        #[cfg(all(feature = "rustls", feature = "native-tls"))]
        {
            return self
                .chain(RustlsConnector::default())
                .chain(NativeTlsConnector::default());
        }

        #[cfg(feature = "rustls")]
        {
            return self.chain(RustlsConnector::default());
        }

        #[cfg(feature = "native-tls")]
        {
            return self.chain(NativeTlsConnector::default());
        }

        panic!("Either the `rustls` or `native-tls` feature must be enabled on arti_ureq.");
    }

Is this okay?

that looks right. but I think you can write it more concisely like this:

/// Returns connector chain depending on features flag.
#[allow(unreachable_code)]
fn connector_chain(self) -> impl UreqConnector {
    let chain = self;

    #[cfg(feature = "rustls")]
    let chain = chain.chain(RustlsConnector::default());

    #[cfg(feature = "native-tls")]
    let chain = chain.chain(NativeTlsConnector::default());

    chain
}
1 Like

Awesome, thanks!