Skip to content

types: adopt derive_more for newtype boilerplate #48

Description

@mfw78

From a workspace consistency audit of hand-rolled newtype trait impls against the derive_more crate, in the reth/alloy idiom (zero-cost newtypes, derives over hand-rolled forwarding).

Why

derive_more is already declared as a workspace dependency (root Cargo.toml:102, version = "2", default-features = false, features = ["full"], with the comment at :98 naming "newtype boilerplate (Deref, From, Display, ...)"). First-party adoption is currently zero: every in-tree reference in Cargo.lock is transitive via alloy. The dependency is already paid for in the build and lock graph, so the first real use costs nothing new and sets the idiom for future newtypes.

The poster child is VenueId(String) (crates/videre-host/src/registry.rs:59), which hand-rolls From<String>, From<&str>, and Display, each a mechanical forward that a derive reproduces. This tracks replacing that boilerplate with derive_more derives, in the alloy-primitives style where thin wrappers lean on derive-generated forwarding rather than hand impls.

Scope is deliberately narrow. The workspace has very few tuple newtypes, and an exhaustive sweep found exactly one clean first-party candidate plus one optional low-value rider in a test-support crate. This is not a broad sweep; it is a targeted rationalization that establishes the pattern.

Scope

Layer Type File:line Current impls derive_more derives
L2 videre-host VenueId(String) crates/videre-host/src/registry.rs:59 From<String> (:68), From<&str> (:74), Display (:80) #[derive(From, Display)] with #[from(forward)] on the field and #[display("{_0}")]
L1 test-support (optional) FieldValue (enum) crates/nexum-sdk-test/src/lib.rs:466 5-arm Display (each variant renders its single field) #[derive(Display)] (single-field variants defer to inner Display; #[display("{_0}")] optional)

Notes on the VenueId swap:

  • Plain #[derive(From)] generates only From<String>. From<&str> is used at real call sites (client.rs, registry.rs str literals like "cow", "unlisted"), so #[from(forward)] is mandatory: it emits From<T: Into<String>>, covering both existing impls. This is a strict superset (also admits Box<str>, Cow), behaviour-preserving at every existing call site.
  • Keep the inherent as_str(&self) -> &str (:63); derive_more has no inherent-accessor derive, and adding AsRef<str> would change the API surface, so it is out of scope.
  • Display via #[display("{_0}")] is a transparent delegation to the inner Display, which forwards formatter flags (width, precision, fill, align). This is a superset of the hand-rolled f.write_str(&self.0), which ignores flags. It is observationally identical here because every current call site is flag-free ({venue}, %venue, %venue_id), verified. Describe it as a transparent-Display superset, not a byte-equivalent swap.

Not in scope

  • SubmitQuota::new (crates/nexum-runtime/src/engine_config.rs:77) and the sibling WatchLimit::new (:110) are const fn. derive_more::Constructor emits a non-const new, which narrows the API. Excluded.
  • All other hand-rolled From/Display impls across nexum-runtime, nexum-venue-sdk, nexum-sdk, videre-host, and the L3 surface are branching, validating, or field-remapping transforms between distinct types or enum variants (ComponentKind validates against WORKER_KIND, ChainError/Fault classify, faults.rs/body.rs remap variants, header.rs Golden* projections). derive_more cannot reproduce them.
  • Error source chaining stays on thiserror (#[from], #[error(transparent)]). Swapping to derive_more::Error would lose #[error(...)] message derivation for no gain.
  • Do not add Deref/DerefMut to identifier-style newtypes. VenueId is not a smart pointer; clippy discourages Deref for non-pointer wrappers, and the reth/alloy convention reserves it for genuinely pointer-like types. The as_str() accessor is the idiomatic choice.
  • modules/, tools/, and the L1 guest SDK crates contain zero mechanical candidates. No changes there.

Constraints

  • derive_more is already default-features = false, features = ["full"] at the workspace root. From and Display are both no_std-capable derives, so per-derive opt-in via #[derive(...)] keeps the guest crates (wasm32-wasip2) safe. Both target crates here are std host crates, so this is moot for the actual change set.
  • Venue-agnostic gate stays green: derive_more matches none of videre|intent|venue|cow, and neither videre-host nor nexum-sdk-test is reachable from the L1 graphs scanned by scripts/check-venue-agnostic.sh. derive_more is already present transitively in those graphs via alloy.
  • Add derive_more.workspace = true to each touched crate's Cargo.toml (crates/videre-host, and crates/nexum-sdk-test if the optional rider lands).

Timing

Best landed before the M5 three-repo split, as a single monorepo PR. After the carve, the L1/L2/L3 crates fan into three separate repos, so a workspace-wide idiom change is cheaper to do while everything still lives in one tree.

Acceptance criteria

  • VenueId's three hand-rolled impls (From<String>, From<&str>, Display) are replaced by #[derive(From, Display)] with #[from(forward)] and #[display("{_0}")]; the inherent as_str() is retained.
  • crates/videre-host/Cargo.toml gains derive_more.workspace = true.
  • All VenueId call sites (str-literal and String construction, Display interpolation) compile and behave unchanged.
  • Optionally, FieldValue::Display is replaced by #[derive(Display)] with derive_more.workspace = true added to crates/nexum-sdk-test/Cargo.toml.
  • scripts/check-venue-agnostic.sh and the full build/test/clippy battery pass.
  • No Deref/DerefMut, no Constructor, and no derive_more::Error are introduced.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions