Skip to content

Custom owned types: #[buffa(remote = ...)] derive to remove the foreign-type newtype tax #212

Description

@iainmcgin

Summary

Add a #[derive(...)] #[buffa(remote = "...")] macro (the serde(remote = ...) pattern) that generates the crate-local newtype and its forwarding impls needed to use a foreign type as a custom owned field type, collapsing today's hand-written boilerplate to one annotation.

Background

The owned Rust type for string/bytes (#206), repeated (#208), map (#210), and message-field/oneof pointers (#209) is now pluggable. A custom type implements a small buffa-owned trait (ProtoString / ProtoBytes / ProtoList / MapStorage / ProtoBox).

The friction is the orphan rule: a foreign type (e.g. ecow::EcoString, smallvec::SmallVec, indexmap::IndexMap) cannot implement a buffa-owned trait directly. The integrator must wrap it in a crate-local newtype and hand-write the trait impl plus the supporting impls (Deref, From both directions, FromIterator/PartialEq/serde/Arbitrary/ReflectList/ReflectMap as applicable). That's correct and consistent across the family, but it's ~30-50 lines of mechanical code per type. A type the integrator owns needs no newtype (foreign-trait-on-local-type is allowed), so this tax falls only on foreign types.

Proposal

A derive/attribute macro that emits the newtype and the forwarding impls from one annotation, e.g.:

#[derive(buffa::ProtoString)]
#[buffa(remote = "ecow::EcoString")]
pub struct MyEcoString(ecow::EcoString);

generating the trait impl + Deref + From (both directions) + the std derives + (feature-gated) serde / Arbitrary / reflection forwarders. One macro family can cover ProtoString / ProtoBytes / ProtoList / MapStorage / ProtoBox.

This is the long-tail counterpart to the existing curated newtype crate (buffa-smolstr): the crate stays the worked example, the macro generalizes it.

Notes / open questions

  • New proc-macro crate (or a feature on an existing one). Keep it optional so non-custom users don't pay for it.
  • Decide which forwarders are mandatory vs feature-gated (reflection is std-only; serde is JSON-only; Arbitrary is fuzz-only).
  • Explicitly not a blanket impl (e.g. impl<T: AsRef<[u8]>> ProtoBytes for T): a blanket consumes the coherence space for a foundational trait and is the wrong trade — the macro generates per-type newtypes instead.

Follow-up to #156; pick up after #208 / #209 / #210 merge.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions