| name | support-3rdparty-traits |
| description | This skill should be used when the user asks to "support a trait from another crate", "implement a third-party trait", "add an integration for crate X", "add num-traits / serde / rand / ... support", "add a feature flag for a new dependency", "wire up crate X behind a feature", "expose type X for crate Y", or "support a new major version of rand / serde / num-traits / diesel / postgres-types" (e.g. "add rand_v09 alongside rand_v08", "support diesel v3"). It provides the SOP for adding an optional third-party dependency behind the project's versioned-feature-flag policy, placing the trait impls in the right module, forwarding the feature across crates and the meta crate, and keeping the no_std/MSRV/changelog invariants intact. |
Supporting Third-Party Traits in dashu
Standard operating procedure for two related changes:
- Branch A — implement traits from a new third-party crate (e.g. add
borsh, arbitrary, bytemuck support, or a trait crate the repo doesn't depend on yet).
- Branch B — support a new major version of a crate that is already integrated (e.g. add
rand_v09 alongside rand_v08, or diesel_v3 alongside diesel_v2).
Both share the same invariants; Branch B reuses Branch A's steps plus a versioning layer.
Repository context (load-bearing facts)
Read these before touching anything — they are the conventions every existing integration follows.
- Impls live in
<crate>/src/third_party/, one file per external crate. <crate>/src/third_party/mod.rs gates each submodule with #[cfg(feature = "...")]. <crate>/src/lib.rs declares mod third_party; (private) then pub use third_party::*; to expose any public items.
- Two visibility flavors in
mod.rs:
pub mod rand; — when the file defines new public types (e.g. UniformBits, UniformUBig) that users import from crate::rand.
mod num_traits; / mod serde; — when the file only impls an external trait for a local type; the impl is globally reachable once compiled, so nothing public needs re-exporting.
- "Stable" vs "unstable" dependencies (this split is explicit in every
Cargo.toml, marked # stable dependencies / # unstable dependencies):
- Stable = the crate's API is essentially frozen across versions (serde, zeroize, num-order). One feature, named after the crate. Example:
serde = ["dep:serde", "dashu-int/serde"].
- Unstable = breaking changes between major versions (rand, num-traits, num-integer, diesel, postgres-types). Always a versioned feature/dep with a
_vXX suffix, plus an unversioned alias feature pointing at the current default.
- Versioned dependency = renamed optional dep. The dep key carries the suffix and
package = resolves it to the real crate:
rand_v08 = { optional = true, version = "0.8.3", package = "rand", default-features = false }
In Rust code you import by the suffixed key: use rand_v08::{Rng, ...};. Inside a file you may alias it back to the natural name for readability: use num_traits_v02 as num_traits;.
- Versioned feature enables its own dep AND forwards downstream. Because float/rational re-export int's types, their feature must also turn on the matching feature in the lower crate:
num-traits_v02 = ["dep:num-traits_v02", "dashu-int/num-traits_v02"]
- Unversioned alias points at the latest version:
rand = ["rand_v08"], num-traits = ["num-traits_v02"]. Users write features = ["rand"]; the alias decides which real version that means.
- Multiple major versions coexist as peers (see diesel):
diesel_v1 and diesel_v2 are both real, each with its own versioned dep + feature + module file (third_party/postgres/diesel_v1.rs, diesel_v2.rs). Shared conversion logic lives in third_party/postgres/mod.rs; each version file only wires that logic to its own diesel traits.
- Private helper deps use a leading-underscore key and are never given their own user-facing feature. Enabled as a side effect:
num-order = ["dep:num-order", "dep:_num-modular"] (rational).
- no_std: every optional dep is declared
default-features = false so it cannot drag std into the default (no_std) build. If the dep itself needs std, the feature must pull in std (e.g. postgres-types_v02 = ["dep:postgres-types_v02", "dep:_bytes", "std"]).
- Meta crate
dashu (root Cargo.toml) forwards every feature to all sub-crates that have it — both the versioned form and the unversioned alias: rand_v08 = ["dashu-int/rand_v08", "dashu-float/rand_v08", "dashu-ratio/rand_v08"] and rand = ["dashu-int/rand", ...].
- MSRV is 1.68. A new dep must build on 1.68; if it doesn't, it must be stripped from MSRV builds via
.github/workflows/drop_incompatible_deps_for_msrv.py (see pre-publish-check skill, Step 6).
- Two
Cargo.toml styles coexist in the repo: integer uses the legacy style where optional = true deps auto-create an implicit feature (so serde/zeroize/num-order aren't listed in [features]). float/rational use the modern explicit style with dep: syntax. Prefer the dep: (modern) style for any new work; match the surrounding crate's existing style when editing an existing feature.
Decision: which branch?
- The crate is not yet in any
Cargo.toml → Branch A.
- The crate is already integrated and you're adding a second major version (or replacing the supported one) → Branch B.
Branch A — Support a trait from a new third-party crate
Step A1 — Classify the dependency: stable or unstable?
If the upstream crate has a history of breaking changes between major versions (rand, num-traits, num-integer) or is pre-1.0, treat it as unstable → you need the versioned suffix machinery. If its API is effectively frozen at 1.x (serde, zeroize, num-order) → stable → a single unversioned feature suffices. When in doubt, choose unstable; the cost is one extra alias line and you avoid a painful migration later.
Step A2 — Declare the optional dependency in the target crate's Cargo.toml
Under the matching # stable dependencies or # unstable dependencies comment. Always default-features = false.
Stable:
borsh = { optional = true, version = "1.x", default-features = false }
Unstable (note the _vXX key + package =):
arbitrary_v1 = { optional = true, version = "1.x", package = "arbitrary", default-features = false }
Step A3 — Define the feature flag(s)
Stable (modern dep: style):
borsh = ["dep:borsh"]
Unstable — two entries: the versioned feature (with cross-crate forwarding if a lower crate also exposes the type) plus the unversioned alias:
arbitrary_v1 = ["dep:arbitrary_v1"]
arbitrary = ["arbitrary_v1"]
If the trait must also be enabled on a crate this one depends on (because it re-exports that crate's types), add the downstream feature to the versioned feature list — see load-bearing fact above.
Step A4 — Create the implementation module
Add <crate>/src/third_party/<crate_name>.rs. Import the external crate by its dep key (alias back to the natural name if it reads better). impl the external trait(s) for the local types (UBig/IBig/FBig/RBig). Follow existing files as a template: integer/src/third_party/num_traits.rs (pure trait impls), integer/src/third_party/rand.rs (defines new public types + a module-level # Examples doc), float/src/third_party/postgres/ (multi-version, shared logic).
If the file defines new public items the user will name, declare it pub mod in mod.rs; otherwise plain mod.
Step A5 — Wire it into third_party/mod.rs
#[cfg(feature = "borsh")]
mod borsh;
#[cfg(feature = "arbitrary_v1")]
pub mod arbitrary;
No change to lib.rs is needed beyond the existing pub use third_party::*; — that line already re-exports anything pub from these modules.
Step A6 — Forward the feature across crates (if applicable)
If the same trait makes sense on types from more than one sub-crate, repeat Steps A2–A5 in each crate, and make each higher crate's versioned feature turn on the lower crate's matching feature (load-bearing fact). Keep the feature name identical across crates (arbitrary_v1 everywhere).
Step A7 — Forward the feature in the meta crate
In the root Cargo.toml [features], add both forms (versioned + unversioned alias), each forwarding to every sub-crate that implements it:
arbitrary_v1 = ["dashu-int/arbitrary_v1", "dashu-float/arbitrary_v1", "dashu-ratio/arbitrary_v1"]
arbitrary = ["dashu-int/arbitrary", "dashu-float/arbitrary", "dashu-ratio/arbitrary"]
List only the sub-crates that actually have the feature — do not forward to a crate that doesn't implement it (that's a build error).
Step A8 — Tests, doc examples, dev-dependencies
- Add a
[[test]] with required-features = ["<feature>"] (see the random / serde tests in each Cargo.toml), or put unit tests in a #[cfg(test)] mod tests at the bottom of the new module file.
- Re-declare the dep (un-suffixed or suffixed to match) under
[dev-dependencies] without optional, so doctests/examples resolve.
- Any public item documented at module level (like
rand.rs) needs a # Examples block; in examples use the dep by its dep key (use rand_v08::{...}).
Step A9 — MSRV check
Confirm the new dep's rust-version (or last-known-compatible release) is ≤ 1.68. If not, add it to drop_incompatible_deps_for_msrv.py so MSRV CI builds strip it.
Step A10 — Changelog
Add an entry under ### Add in the ## Unreleased section of every crate whose Cargo.toml you changed (target crate, any crate that got cross-crate forwarding, and note the meta crate has no changelog). New optional deps are additive → patch bump.
Branch B — Support a new major version of an existing crate
Example below uses adding rand_v09 alongside rand_v08; substitute names as needed.
Step B1 — Add the new versioned dependency
Keep the old one; add the new one beside it under # unstable dependencies:
rand_v08 = { optional = true, version = "0.8.3", package = "rand", default-features = false }
rand_v09 = { optional = true, version = "0.9.0", package = "rand", default-features = false }
Both keys resolve to the real rand crate via package =. They coexist fine because the keys differ.
Step B2 — Add the versioned feature
In every crate that supports it, mirroring the old version's feature line and its cross-crate forwarding:
rand_v09 = ["dep:rand_v09", "dashu-int/rand_v09"]
rand_v09 = ["dep:rand_v09"]
Step B3 — Decide the unversioned alias target
The alias rand = [...] selects the default version users get. Two options:
- Keep the alias on the old version (
rand = ["rand_v08"]) — purely additive, no behavior change for existing features = ["rand"] users. Recommended default.
- Move the alias to the new version (
rand = ["rand_v09"]) — existing users silently upgrade. This is a breaking change in effect; only do it deliberately, call it out in the changelog under ### Change, and require at least a minor bump (pre-1.0 rule).
If you move the alias, the old versioned feature still exists for users who need to pin it explicitly.
Step B4 — Create/adapt the implementation module
Add <crate>/src/third_party/rand_v09.rs (or whatever splits cleanly). Often you can share logic with the old-version file by extracting the version-independent core into a helper (the postgres/mod.rs Numeric struct + conversion fns are the model: shared core, version-specific trait wiring in diesel_v1.rs/diesel_v2.rs). Gate the new module in mod.rs:
#[cfg(feature = "rand_v08")]
pub mod rand;
#[cfg(feature = "rand_v09")]
pub mod rand_v09;
Port every trait impl from the old file, adjusting for API differences in the new major version.
Step B5 — Cross-crate + meta forwarding
Repeat Step B2 in every implementing crate, and add rand_v09 = [...] (and, if you moved the alias, the updated rand = [...]) to the root Cargo.toml forwarding to all sub-crates that have it.
Step B6 — Tests, doc examples, dev-dependencies
Add a dev-dependency for the new version, and either a new [[test]] gated on rand_v09 or extend the existing one to cover both versions. Mirror the old version's doctest coverage using the new dep key.
Step B7 — Changelog
Under ### Add in each affected crate's ## Unreleased: "Add support for rand 0.9 (behind the rand_v09 feature)." If you moved the unversioned alias (Step B3 option 2), also add a ### Change entry and treat the release as breaking.
Verification
After either branch, confirm the feature compiles in isolation and that the no_std path still holds:
cargo check -p <crate> --features <feature>
RUSTFLAGS='--cfg force_bits="32"' cargo check -p <crate> --features <feature>
cargo build -p <crate> --no-default-features --features <feature>
cargo clippy -p <crate> --features <feature> -- -D warnings
cargo test -p <crate> --features <feature> --doc
cargo check -p dashu --features <feature>
If the feature exists in multiple sub-crates, run the --features check on each and on -p dashu.
Common failure modes
- Forgot the
package = rename on a versioned dep — rand_v09 without package = "rand" makes cargo look for a (nonexistent) crate literally named rand_v09. Every versioned dep needs package = "<real crate name>".
- Forgot cross-crate forwarding — float/rational's
num-traits_v02 feature must list dashu-int/num-traits_v02; without it, enabling the feature on the higher crate doesn't give the lower crate's types the impl.
- Forwarded a feature to a crate that doesn't have it —
dashu-base has no rand feature; listing it in the meta crate's rand forwarding is a build error. Only forward to crates that actually define the feature.
- Dep without
default-features = false — silently breaks the no_std build (serde/zeroize pull std by default). Every optional dep in this repo sets it.
#[cfg] feature name ≠ Cargo feature name — mod.rs gates on the feature name (rand_v08), not the dep key's package. They usually match the key, but if you renamed via package =, the cfg still uses the key/feature name, not rand.
- Added an unstable dep without the unversioned alias — users expect
features = ["rand"] to work; a versioned-only feature breaks that contract.
- New dep exceeds MSRV — builds fine on stable CI, fails the
1.68 matrix job. Must be stripped in drop_incompatible_deps_for_msrv.py.
- Visibility mismatch — declared
mod foo in mod.rs but the module defines public types the user needs (crate::foo::UniformBits). If users name items from the module, it must be pub mod.
- Missed a changelog — every
Cargo.toml you edited (target crate + forwarded crates) needs an ## Unreleased entry; the meta crate has none.