Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 decodeWireEncode and WireDecode<'de>, or the one-trait WireScalar form for a leaf;
  • a shapeWireShape, the recursive Shape the field binding stores for the type, which the model-driven walker and fixture tooling read;
  • a placeholderRedact, which the WireModel derive requires of every non-skip field 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 bindingWireModel 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 codecto_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.