Skip to content

← Modules, Visibility, Testing and Docs step 17 of 22

Hard Framework

SemVer for Rust APIs: what actually breaks

Compare two simplified descriptions of a Rust API and classify the change as "major", "minor" or "patch".

pub fn classify(before: Vec<String>, after: Vec<String>) -> String

Apply the rules below and return the most severe result: major takes precedence over minor, which takes precedence over patch. These rules define this exercise's model. They are not a complete predictor of compatibility for arbitrary Rust code.

Parse entities, not whole lines

Each description contains facts in this format:

struct Name exhaustive
struct Name non_exhaustive
enum Name exhaustive
enum Name non_exhaustive
field Struct.name pub
field Struct.name private
variant Enum.Name
trait Name
method Trait.name required
method Trait.name defaulted
fn name Clone+Send
fn name -

A type has one of the two exhaustiveness markers; a field has one visibility; a method is either required or defaulted. - means no function bounds. Bound names separated by + form a set, so Clone+Send and Send+Clone are equivalent. Treat bound names as independent labels rather than resolving Rust trait implications.

Inputs use the listed forms with consistent entity definitions and owners. Whitespace between tokens and line order do not affect the classification. An empty description is allowed. Identical repeated facts have no effect. The model includes private fields because their presence affects whether outside code could construct a struct literal.

Identify a type by its kind and name, a field/variant/method by its path, and a trait or function by its name. For example, changing struct S exhaustive to struct S non_exhaustive changes one type's attribute. It does not remove that type and introduce an unrelated one.

Apply the exercise's rules

Start with patch. Any of the following findings can raise that level.

Change Classification
Remove any recorded entity, including a private field major
Add a type, trait or function minor
Add a field to a newly introduced struct minor
Add a field to an existing struct that previously had a private field minor
Add a field when the resulting struct is non-exhaustive minor
Add any other field to an existing struct major
Add a variant to a new enum or a resulting non-exhaustive enum minor
Add a variant to an existing exhaustive enum major
Add a required method to an existing trait major
Add a defaulted method, or any method to a new trait minor
Change a method from required to defaulted minor
Change a method from defaulted to required major
Change a type from exhaustive to non-exhaustive major
Change a type from non-exhaustive to exhaustive minor
Change a field from public to private major
Change a field from private to public minor

For a function that exists in both descriptions, compare its bound sets:

  • Equal sets: no change.
  • The new set is a proper subset of the old one: minor.
  • Any other change: major. This includes adding a bound or replacing one bound with another.

All rules apply together. Adding a field may qualify as minor because the resulting struct is non-exhaustive, but adding that attribute at the same time is itself major. The combined answer is therefore major.

If no rule reports a change, return patch. Reordering lines or bound names alone should not raise the level.

Why the structural distinctions matter

Adding a field to an all-public exhaustive struct can break outside struct literals and patterns that list all fields without ... If the struct already has a private field, outside code already cannot construct it with a literal. A non-exhaustive struct also prevents that construction outside its defining crate.

Adding a variant to an exhaustive enum can break a match that previously covered every variant. A non-exhaustive enum already requires an outside match to allow for unknown variants. Inside the defining crate, this attribute does not impose those external construction and matching limits.

These explain the exercise's rules, but do not establish that a private field change is always compatible in a real library.

Where real compatibility needs more information

A public API includes more than reachable names: signatures, trait implementations, auto traits and documented behavior can also matter. This input language does not describe all of them.

For example, adding a private Rc field can make a public type stop being Send, breaking callers that move it between threads. The classifier cannot detect this because field types are absent. Conversely, removing a private field does not necessarily remove any callable public path; the model's blanket removal rule deliberately does not analyze that distinction.

The model classifies an added defaulted method as minor. In real code it can introduce ambiguity between methods from two traits in scope. A matching inherent method generally takes precedence rather than automatically causing that ambiguity. Trait-object compatibility is another consideration, as discussed in the sealed-trait exercise.

The minimum supported Rust version is also outside this model. Raising it can exclude users of older compilers. Cargo's rust-version metadata and resolver policy help select compatible dependency versions when available; they do not guarantee that every dependency constraint has a compatible solution or decide your project's release policy.

Use compatibility tools, downstream compile checks and behavior tests as complementary evidence. No structural classifier proves every update safe. Consult the Cargo compatibility guide when reviewing an actual release.

Repair the starter

The starter compares sets of complete lines. It mistakes an attribute or bound change for a removal, and treats every addition as minor. Parse the facts into maps and sets, then apply the entity-specific rules.

Passing requires compilation, expected classifications and Clippy with warnings treated as errors.