Hand-Written Impls and the Value Tree
The attributes are the front door, not the only door. This chapter collects the ways out: a leaf type that owns its own wire form, a fully hand-written impl, a runtime value tree for shapes you can’t type, and the two knobs for generic and renamed-crate situations.
What a field type owes the derive
Before the two hatches, the contract they satisfy. A type used as a field of a derived model needs four things, and the derive asks for each by name:
- encode and decode —
WireEncodeandWireDecode<'de>, or the one-traitWireScalarform for a leaf; - a shape —
WireShape, the recursiveShapethe field binding stores for the type, which the model-driven walker and fixture tooling read; - a placeholder —
Redact, which theWireModelderive requires of every non-skipfield type (Value Classes and Redaction).
Derived models get all four from their derives. A hand-written type supplies them by
hand — two identity methods for Redact when the type holds nothing sensitive, and a
Shape that is at least as permissive as the type’s own WireDecode.
WireScalar: a leaf that owns its form
When a leaf value has a representation no scalar rule expresses, implement
WireScalar and derive the encode/decode with the scalar container attribute. A
scalar maps to exactly one of the model’s leaf lanes per emission — bool, the
integer and float lanes, str, bytes — plus a semantic ScalarKind. Lane is
syntax and KIND is semantics: a text format renders whichever lane the scalar
writes, while a binary format keys off KIND to choose a native encoding.
capi_wire_decimal’s Decimal is the shipped model of the shape:
#[derive(Debug, Clone, Copy, Default, WireEncode, WireDecode)]
#[wire(scalar)]
pub struct Decimal { coeff: i128, exp: i16 }
impl WireScalar for Decimal {
const KIND: ScalarKind = ScalarKind::Decimal;
fn write_wire(&self, out: &mut dyn ScalarSink) -> Result<(), CodecError> {
out.put_number(&self.to_plain_string()) // one lane: the number lane
}
fn read_wire(source: ScalarSrc<'_>) -> Result<Self, CodecError> {
/* parse the number lane, or the text lane a quoting API sends */
}
}
capi_wire_model::impl_wire_shape_scalar!(Decimal); // Shape::Scalar(ScalarKind::Decimal)
impl Redact for Decimal {
fn redacted(self) -> Self { self }
fn redacted_placeholder(self) -> Self { Self::new(0, 0) }
}
ScalarSink is the sink a scalar writes into — streamed put_str fragments, or one
one-shot lane such as put_number, put_i64, put_bool, put_bytes — and
ScalarSrc is the leaf it reads back from. The two halves must agree on one lane, or
the type cannot survive a codec that keeps that lane native: writing put_i64 while
reading only ScalarSrc::Str round-trips through a text format and fails against a
value tree or a binary codec. ScalarKind names the semantics — the lane kinds
Bool, I64, U64, F64, Str, Bytes, then DateTime, Date, Time,
Duration, Decimal, Uuid, and Custom(&'static str) — for the formats that need it.
The derives then emit only the encode_scalar/decode_scalar delegations to the
codec’s Encoder/Decoder, bounded on Self: WireScalar. A scalar type carries no
binding — WireModel rejects the attribute — and no other wire attribute is allowed
anywhere on the item, with the single exception of #[wire(crate = "...")], which
steers path resolution rather than wire shape.
Reach for this when the type is a leaf that appears in many places. For a one-off
field, a via proxy is less machinery.
Fully hand-written impls
When no combination of attributes expresses the shape, implement the traits
directly. grok’s MaxResponseOutputTokens is a wire value that is a union — an
integer bound, null for unbounded, or an "inf" string on server-sent session
events — which no single scalar lane can name:
pub struct MaxResponseOutputTokens(Option<i32>);
impl WireEncode for MaxResponseOutputTokens {
fn encode<E: Encoder>(&self, encoder: E) -> Result<E::Ok, E::Error> {
self.0.encode(encoder) // an integer bound, or null when unbounded
}
}
impl<'de> WireDecode<'de> for MaxResponseOutputTokens {
fn decode<D: Decoder<'de>>(decoder: D) -> Result<Self, D::Error> {
/* decode_any, then accept an integer, null, or the "inf" text form */
}
}
// `Any` keeps the walker exactly as permissive as this type's own decode, so
// fixture tooling accepts every payload the typed path does.
impl WireShape for MaxResponseOutputTokens {
const SHAPE: Shape = Shape::Any;
}
// The bound masks to zero; the unbounded state stays unbounded, since `None` is
// what omits the field, and inventing a `Some` would add a limit the request never sent.
impl Redact for MaxResponseOutputTokens {
fn redacted(self) -> Self { self }
fn redacted_placeholder(self) -> Self { Self(Redact::redacted_placeholder(self.0)) }
}
This is the same hatch JWT’s claims
use for their runtime-keyed map. You are writing against the same Encoder/Decoder
traits every codec implements, so a hand-written type works with every format — and
the WireShape you declare is the promise the walker holds you to: state a shape at
least as permissive as what decode accepts, and Shape::Any when the wire form is a
union.
The value tree
Sometimes a field’s shape is intentionally open — a spec field, a passthrough blob, a
vendor extension. capi_wire_value::Value is the self-describing runtime tree for
exactly that: you decode into it when you don’t have, or don’t want, a static type, and
the codec reports whatever is on the wire.
pub struct Response {
pub id: String,
pub raw: Value, // whatever the service sent
}
Value implements WireEncode/WireDecode/DynEncode — and WireShape as
Shape::Any and Redact, so it can be a field of a derived model — and round-trips
through any codec. Numbers are split by sign and width (I64 / U64 / I128 /
U128 / F64) to mirror the wire rather than collapsing to one numeric type, with a
Number(String) lane for the exact decimal text a fraction or a beyond-128-bit
integer arrives as; Bytes holds a byte string; objects keep their entries in
insertion order. SchemaDocument is a transparent newtype over it for spec fields
whose shape is deliberately open.
Two other things ride on the same tree: the enum
capture policy decodes into it, and the test
framework’s value-first fixture comparison walks it. There is also an in-memory
codec — to_value and from_value move between typed wire values and the tree with
no byte format in the middle, applying the same binding metadata (names, scalar rules,
wraps, flatten, tagging) a self-describing byte codec would.
One codec-specific note: a tree entry is a plain (key, value) pair, which cannot say
where XML put something — so the XML codec’s self-describing form spells placement in
the key. @name is an attribute of the enclosing element, #text is its text (mixed
content concatenates to one entry), and any other key is a child element. XML’s own
grammar makes the markers collision-free — neither @ nor # can start an XML name —
and the codec reads them back into placement on encode, so a captured element
round-trips, and a Value::Object (or a typed map field) written under XML describes
its element’s content the same way.
Generics
Type and const parameters work on structs and enums. The generated impls bound exactly
the parameters a non-skip field or variant payload uses — T: WireEncode /
T: WireDecode<'de> — so a phantom parameter stays unbounded, mirroring serde’s
inference. Override it when inference isn’t what you want:
#[wire(bound = "T: MyTrait")]
#[wire(bound(encode = "…", decode = "…"))] // or set each direction separately
Predicates are written without the where keyword. Three constraints are worth knowing:
WireDecode cannot be derived for a type with lifetime parameters (decoding yields
owned data), though WireModel and WireEncode accept them; and two enum shapes must
stay non-generic — external struct variants and #[wire(string_enum)].
Crate path
Generated code resolves wire-model items through ::capi_wire_model. When the crate
isn’t a direct dependency under that name — renamed in Cargo.toml, or reached through
a facade — redirect it:
#[derive(WireModel, WireEncode, WireDecode)]
#[wire(crate = "my_facade::wire_model")]
struct Order { id: u64 }
The value must parse as a Rust path and is emitted verbatim, deliberately without a
leading ::, so an alias in scope at the derive site (use capi_wire_model as wire;
with crate = "wire") works too. All three derives read the one attribute written on the
type. There is a matching #[wire(datetime_crate = "...")] for the datetime adapters,
defaulting to ::capi_wire_datetime.
This is also why a client crate lists capi_wire_model among its own dependencies,
under that name, rather than reaching it through another crate — see the manifest
chapter.
That closes the wire model. With types that know how to cross the wire, the next part builds the endpoints that carry them.