open-standards/joule-code/joule-code-rs/crates/jwp/src/encoding.rs
Transaction Science 08c499f8ca joule-code: port CommunicationOS transport (jwp + joule-moq) into the public standard
The MoQT transport that joule-code-collab's network rung needs lived in the
private joulesperbit repo (as inv-moq + jwp), so a public consumer couldn't
depend on it. Port both into the public open-standards joule-code workspace,
self-contained (concrete dep versions, not workspace-inherited) so a bare git
clone builds them:

- crates/jwp: Joule Wire Protocol — energy-aware binary framing (CBOR, zstd/lz4,
  TCP + optional QUIC). Self-contained, external deps only.
- crates/joule-moq: MoQT (Media-over-QUIC) wire protocol + JWP signed-receipt
  extension for CommunicationOS — Publisher/Subscriber/Relay traits, MockFabric
  (in-process) + a production QUIC path (quic feature). Renamed from inv-moq
  to drop the invisible-* namespace and match the joule-code branding; depends
  only on jwp.

Both build (default + quic) and pass their tests (joule-moq 29, jwp 152) in the
new home. The transport-moqt example now path-deps the public joule-moq instead
of reaching into joulesperbit, and its end-to-end BFT-ops-over-MoQT-with-signed-
receipts tests pass. This unblocks public consumers (agent-zed) from git-depping
the transport for cross-machine fleet coordination.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-13 21:29:05 -04:00

156 lines
5.3 KiB
Rust

//! Pluggable payload encoding for JWP frames.
//!
//! The [`PayloadCodec`] trait abstracts payload serialization so the
//! protocol can swap encodings per-frame based on adaptive decisions.
use serde::{Serialize, de::DeserializeOwned};
use crate::error::JwpError;
// ── Encoding identifier ──────────────────────────────────────────
/// Identifies the payload encoding used in a frame.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum EncodingId {
/// Canonical CBOR (RFC 8949) — the default and only v1 encoding.
Cbor = 0x01,
// Future: FlatBuf = 0x02, Raw = 0x03, MsgPack = 0x04
}
impl EncodingId {
/// Parse from wire byte.
pub fn from_u8(b: u8) -> Option<Self> {
match b {
0x01 => Some(Self::Cbor),
_ => None,
}
}
}
// ── Trait ─────────────────────────────────────────────────────────
/// Trait for pluggable payload serialization.
///
/// Implementations must be `Send + Sync` so they can be stored in
/// codecs shared across async tasks.
pub trait PayloadCodec: Send + Sync {
/// Serialize a value to wire bytes.
fn encode_payload<T: Serialize>(&self, value: &T) -> Result<Vec<u8>, JwpError>;
/// Deserialize wire bytes back to a value.
fn decode_payload<T: DeserializeOwned>(&self, data: &[u8]) -> Result<T, JwpError>;
/// The encoding identifier for this codec.
fn encoding_id(&self) -> EncodingId;
/// Estimated per-payload overhead in bytes (for adaptive decisions).
///
/// This is the typical number of bytes that the encoding format
/// adds beyond the raw field values. Used by the [`ConnectionProfile`]
/// to decide when a lighter encoding would save energy.
fn estimated_overhead_bytes(&self) -> usize;
}
// ── CBOR implementation ──────────────────────────────────────────
/// The default CBOR codec — wraps the existing `cbor_encode`/`cbor_decode`.
///
/// Uses canonical deterministic CBOR (RFC 8949) via `ciborium`.
#[derive(Debug, Clone, Copy)]
pub struct CborPayloadCodec;
impl PayloadCodec for CborPayloadCodec {
fn encode_payload<T: Serialize>(&self, value: &T) -> Result<Vec<u8>, JwpError> {
crate::frame::cbor_encode(value)
}
fn decode_payload<T: DeserializeOwned>(&self, data: &[u8]) -> Result<T, JwpError> {
crate::frame::cbor_decode(data)
}
fn encoding_id(&self) -> EncodingId {
EncodingId::Cbor
}
fn estimated_overhead_bytes(&self) -> usize {
// CBOR adds ~15-30 bytes of type/length metadata for typical
// search payloads (map keys, string length prefixes, etc.)
20
}
}
// ── Tests ────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
use crate::frame::{HandshakePayload, QueryPayload, ResultPayload};
#[test]
fn cbor_roundtrip_query_via_trait() {
let codec = CborPayloadCodec;
let payload = QueryPayload {
query: "rust async runtime".into(),
limit: 25,
session_id: Some("sess-42".into()),
};
let bytes = codec.encode_payload(&payload).unwrap();
let decoded: QueryPayload = codec.decode_payload(&bytes).unwrap();
assert_eq!(decoded.query, "rust async runtime");
assert_eq!(decoded.limit, 25);
assert_eq!(decoded.session_id.as_deref(), Some("sess-42"));
}
#[test]
fn cbor_roundtrip_result_via_trait() {
let codec = CborPayloadCodec;
let payload = ResultPayload {
rank: 3,
url: "https://docs.rs/tokio".into(),
title: "Tokio Docs".into(),
domain: "docs.rs".into(),
score: 8.5,
content_hash: "blake3_abc".into(),
};
let bytes = codec.encode_payload(&payload).unwrap();
let decoded: ResultPayload = codec.decode_payload(&bytes).unwrap();
assert_eq!(decoded.rank, 3);
assert_eq!(decoded.url, "https://docs.rs/tokio");
assert_eq!(decoded.domain, "docs.rs");
}
#[test]
fn cbor_roundtrip_handshake_via_trait() {
let codec = CborPayloadCodec;
let payload = HandshakePayload {
version: 1,
capabilities: vec!["search".into(), "cancel".into()],
};
let bytes = codec.encode_payload(&payload).unwrap();
let decoded: HandshakePayload = codec.decode_payload(&bytes).unwrap();
assert_eq!(decoded.version, 1);
assert_eq!(decoded.capabilities, vec!["search", "cancel"]);
}
#[test]
fn encoding_id_and_overhead() {
let codec = CborPayloadCodec;
assert_eq!(codec.encoding_id(), EncodingId::Cbor);
assert!(codec.estimated_overhead_bytes() > 0);
assert!(codec.estimated_overhead_bytes() < 100);
}
#[test]
fn encoding_id_from_u8() {
assert_eq!(EncodingId::from_u8(0x01), Some(EncodingId::Cbor));
assert_eq!(EncodingId::from_u8(0x00), None);
assert_eq!(EncodingId::from_u8(0xFF), None);
}
}