dyn-encoding 0.0.1

Wire-format codec abstraction (protobuf, JSON, CBOR; flatbuffers/capnp/bebop/BSON to follow) for the Riak protocol layer
Documentation
# dyn-encoding

Wire-format codec abstraction for the Riak protocol layer of the
Dynomite Rust port.

The Riak protocol has historically pinned its on-the-wire encoding to
Google Protocol Buffers. Modern Riak deployments and the operator
brief that drives this workspace want to negotiate a richer set of
encodings on a per-request basis, so that clients that already speak
JSON, CBOR, or one of the schema-first contenders (FlatBuffers,
Cap'n Proto, Bebop) can talk to the same server without first
shoehorning their messages through protobuf.

This crate provides the abstraction. It does **not** implement the
Riak protocol itself; that lives in the (forthcoming) `dyniak`
crate and consumes `dyn-encoding` to negotiate the wire format on
each connection.

## Scope

Encoding negotiation applies **only** to the Riak protocol layer.
Redis RESP, Memcached ASCII, and the internal DNODE peer-plane
framing are unchanged.

## Trait surface

Two cooperating traits.

```rust
pub trait WireValue: Debug + Send + Sync + 'static {
    fn wire_type_id() -> WireTypeId where Self: Sized;
}

pub trait WireCodec: Send + Sync + 'static {
    fn content_type(&self) -> &'static str;
    fn encode(&self, value: &dyn ErasedWireValue) -> Result<Vec<u8>, CodecError>;
    fn decode(
        &self,
        type_id: WireTypeId,
        bytes: &[u8],
    ) -> Result<Box<dyn ErasedWireValue>, CodecError>;
}
```

`WireTypeId` is a newtype around `&'static str` keyed into the codec
registry. `ErasedWireValue` is the object-safe view over `WireValue`
that codec implementations consume; a blanket impl on every
`T: WireValue` makes the coercion implicit at call sites.

The `CodecRegistry` maps content-type headers (for example
`application/json`) to `&dyn WireCodec`:

```rust
let mut registry = CodecRegistry::with_baseline();
let codec = registry.for_content_type("application/cbor").unwrap();
```

## Baseline codecs

Seven codecs ship in version 0.0.1.

| Module        | Content-type                                  | Backend       |
|---------------|-----------------------------------------------|---------------|
| `json`        | `application/json`                            | `serde_json`  |
| `cbor`        | `application/cbor`                            | `ciborium`    |
| `protobuf`    | `application/x-protobuf`                      | `prost`       |
| `flatbuffers` | `application/octet-stream;schema=flatbuffers` | `flatbuffers` |
| `capnp`       | `application/capnproto`                       | `capnp`       |
| `bebop`       | `application/x-bebop`                         | `bebop`       |
| `bson`        | `application/bson`                            | `bson`        |

All seven follow the same registration pattern: types are attached
to a codec instance through `register::<T>()` before the codec is
installed in the registry. The bound on `T` differs per codec:

* `JsonCodec::register::<T>()`        -- `T: WireValue + Serialize + DeserializeOwned`
* `CborCodec::register::<T>()`        -- `T: WireValue + Serialize + DeserializeOwned`
* `BsonCodec::register::<T>()`        -- `T: WireValue + Serialize + DeserializeOwned`
* `ProtobufCodec::register::<T>()`    -- `T: WireValue + prost::Message + Default`
* `FlatbuffersCodec::register::<T>()` -- `T: FlatbuffersWire`
* `CapnpCodec::register::<T>()`       -- `T: CapnpWire`
* `BebopCodec::register::<T>()`       -- `T: BebopWire`

The protobuf codec deliberately avoids `prost-build`. The three
schema-first newcomers similarly avoid their respective code
generators (`flatc`, `capnpc`, `bebopc`); each defines a small
per-codec trait so the schema and conversion glue live in the
crate that owns the message types (typically `dyniak`), not in
this codec abstraction. For testing, hand-rolled fixtures live
inline in each codec module's `tests` block.

## Adding a codec

The trait surface is shaped to host new encodings without churn:

1. Add the upstream crate to `[workspace.dependencies]` in the
   root `Cargo.toml`.
2. Add a feature-flagged or unconditional dependency in
   `crates/dyn-encoding/Cargo.toml`.
3. Create `src/codec/<name>.rs` modeled on the JSON codec module.
   The bound on `register::<T>()` is the new format's native trait
   (`flatbuffers::Follow + Push`, `capnp::Owned`, ...), or a
   crate-defined trait if the upstream API does not fit.
4. Add `pub mod <name>;` to `src/codec/mod.rs` and the corresponding
   `pub use` in `src/lib.rs`.
5. Add the codec to `CodecRegistry::with_baseline` if it should be
   on by default.
6. Mirror the test suite from JSON/CBOR/protobuf: round-trip,
   idempotent encode, unknown-type-id (encode + decode),
   malformed-bytes.

The deferred-codec sketches and the on-the-ground deltas are in
`docs/journal/2026-05-24-dyn-encoding-scaffold.md` and
`docs/journal/2026-05-24-dyn-encoding-deferred-codecs.md`.

## Acknowledgements

The Dynomite Rust port descends from Netflix's Dynomite (C). The
encoding-negotiation idea is informed by the design notes at
<https://gist.github.com/MangaD/77dba2f4c7055b35637fb596c175ffb1>;
see that gist for the comparative analysis of the seven encodings.