educe
Procedural macros that derive Rust's built-in traits with field-level customization the standard derives can't express.
Repository Health
Technical Analysis
educe is a Rust proc-macro crate built around a single #[derive(Educe)] entry point that implements Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deref, DerefMut, and Into — the same traits the standard library’s own derives cover, but with per-field and per-variant control that #[derive(...)] alone cannot offer: renaming fields and variants in Debug output, ignoring specific fields, substituting a custom method for a field’s trait logic, formatting a struct as a tuple (or vice versa), and deriving several traits together through one #[educe(...)] attribute list.
A distinguishing feature is its automatic trait-bound inference: rather than emitting the blanket T: Trait bound the standard derives add for every generic parameter, educe walks each field’s type and generates the precise where-clause the implementation actually needs, recursing through wrapper types like Option<T> and Vec<Box<T>>, recognizing types that never need a bound (PhantomData, raw pointers), and falling back to explicit bound(...) syntax when a type’s real requirements can’t be inferred automatically. Related traits also inherit each other’s final predicates within the same attribute, so Ord picks up whatever Eq and PartialOrd already established.
Each trait is gated behind its own Cargo feature (enabled by default, so unused derive logic can be stripped from compile times), and the crate supports unions in addition to structs and enums for several traits — a case the built-in derives don’t handle at all.
What You Get
- One derive macro covering Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deref, DerefMut, and Into, each behind its own opt-in Cargo feature
- Automatic, per-field trait-bound inference that produces tighter where-clauses than the compiler’s blanket generic bounds, with
bound(...),bound(*), andbound(false)escape hatches for cases it can’t infer - Field- and variant-level control: rename or hide names in Debug output, ignore individual fields, or swap in a custom method to override a field’s derived behavior
- Struct-as-tuple and tuple-as-struct formatting for Debug, and support for unions in traits where the standard derives refuse to apply
- Trait inheritance within a single
#[educe(...)]attribute, so derivingEq,PartialOrd, andOrdtogether carriesPartialEq’s bounds forward automatically - no_std compatibility, verified across its own integration test suite
Common Use Cases
- Implementing Debug for types with fields that shouldn’t appear in output (secrets, internal caches) without hand-writing the impl
- Deriving Clone, PartialEq, or Hash on generic types where the compiler’s default derive bounds are too strict or too loose for the type’s actual field requirements
- Building smart-pointer-like wrapper types that need Deref/DerefMut without boilerplate
- Applying comparison or hashing traits to unions, which
#[derive(...)]cannot target directly - Migrating off several single-purpose derive crates (a Debug-only crate, a Deref-only crate, etc.) onto one dependency with a consistent attribute syntax
Under The Hood
Architecture
The crate exposes one proc-macro entry point that parses the #[educe(...)] attribute and dispatches to a per-trait handler module under src/trait_handlers/ — clone, copy, debug, default, deref, deref_mut, eq, hash, into, ord, partial_eq, and partial_ord — each implementing a shared TraitHandler trait (TraitHandlerMultiple for Into, which can target several conversions at once) defined in trait_handlers/mod.rs. A TraitHandlerContext struct threads through the expansion of a single derive, recording the where-predicates each handler actually emits so that later traits in the same attribute can inherit them — Eq and PartialOrd inherit from PartialEq, Ord from both, Copy from Clone. Shared parsing and codegen logic — attribute parsing, bound construction, type-walking, path handling — lives in src/common/, reused by every handler, and a dedicated panic.rs module centralizes diagnostic-error construction so every handler reports failures in a consistent format. Each trait handler is gated behind its own Cargo feature flag, so the module tree compiles only the derive logic a consumer actually enabled.
Tech Stack
A standard Rust proc-macro crate (proc-macro = true in Cargo.toml) built on syn 3, quote 1.0.44, and proc-macro2 1.0.91 for parsing and code generation, plus enum-ordinalize 4.4 for iterating the crate’s internal Trait enum. Targets a recent Rust edition (2024) and toolchain (rust-version 1.89). Dev-dependencies add syn with the full feature and assert-eq-float for the integration test suite. CI runs via GitHub Actions (a standard build/test workflow plus a separate version-check workflow) with Dependabot keeping dependencies current, and the crate publishes documentation to docs.rs with all features enabled.
Code Quality
The crate has no unit tests inside src/, but an extensive integration suite under tests/ — over 30 files — exercises every supported trait against structs, enums, and unions independently, including #![no_std] builds to verify the derived code has no implicit std dependency. Error handling is centralized: rather than ad hoc panics scattered across handlers, panic.rs defines typed syn::Error constructors (unsupported trait, incorrect attribute format, reused trait, unsupported union/unit-variant combinations) that every handler calls into, keeping diagnostic messages consistent across the whole macro surface. Naming follows standard Rust convention throughout, and a rustfmt.toml enforces formatting; no dedicated clippy configuration is visible in the repository root.
API Design
The macro’s surface mirrors Rust’s own derive syntax closely — #[derive(Educe)] plus #[educe(TraitName(options))] — so the learning cost for anyone already using #[derive(...)] is low, and the README documents every parameter with runnable examples. What differentiates it from other “derive more traits” crates is the automatic bound-inference system: instead of the blanket generic bounds the standard derives emit, educe computes the precise where-clause a field’s type actually requires, with documented fallback syntax (bound(*), bound(false), explicit predicates) for cases the inference can’t resolve — a level of sophistication most alternative derive-macro crates don’t attempt.