API change hygiene

Hello,

I would like to share with you my small experience about API changes.

I come from the PHP world, with Symfony who have a nice policy: a new major version only removes deprecated stuff. The promise: updates your project to the last minor version, fix deprecation warnings and you can update to a next major version easy as a composer cargo update.

To do that in rust, I add a new v2 feature. This is easy to use for new struct field, enum variant or trait item according to the API evolution RCF.

For API deletion, we can use the cfg! + deprecated notations.

It remains signature change. In my case, I would like remove unwrap in my foo function and return a Result instead. This is what it looks like:

fn _foo() -> Result<String, ()> {
    todo!()
}

#[deprecated(note = "enable the v2 feature to use the new signature.")]
#[cfg(not(feature = "v2"))]
fn foo() -> String {
    _foo().unwrap()
}

#[cfg(feature = "v2")]
fn foo() -> Result<String, ()> {
    _foo()
}

What do you think? I have miss something?

The problem is that features are additive, so all crates in your dependency tree see the same semver-compatible version of foo with a union of all enabled features. For instance:

  • A main crate depends on abc and xyz.
  • abc depends on foo without specifying features, so they expect fn foo() -> String
  • xyz depends on foo with v2, so they expect fn foo() -> Result<String>
  • foo is built with the union of all enabled features, so v2 is enabled from xyz's requirement.
  • abc will fail to compile because it wasn't ready for the foo/v2 change.

The basic guideline is that enabling a feature should never make breaking changes. It can't be considered an "opt-in" when one part of the dependency tree can force the change on everyone.

5 Likes

You will have to bump semver-major version instead. Cargo allows "foo v1" and "foo v2" to co-exist side by side as if they were different crates.

3 Likes

Opps :frowning_face: Is it the case for all cfg options?

The idea it’s to prevent big update with little API updates at each minor version (or not if you like deprecation warning…).

Incompatible API changes must come with major version bump, aside from the cases when the broken code is always wrong anyway. You can reduce the impact in many cases by using so-called semver trick, to allow multiple major versions to coexist by reexporting everything compatible from the latest one.

6 Likes

Interesting, thank you.