Method Chaining
ergo-sbe encoders are designed so the entire encode reads as one expression,
from wrap_and_apply_header through .fixed(...) and every dynamic tail,
ending in .encoded_length_with_header(). Bind only the resulting length; do not retain
intermediate encoder variables.
Prefer (one chain, one let):
#![allow(unused)]
fn main() {
let mut buf = [0u8; HeartbeatEncoder::compute_length_with_header()];
// Buffer is exact size from const compute_length_with_header — no bounds check needed.
let len = HeartbeatEncoder::try_wrap_and_apply_header(&mut buf, 0)?
.fixed(&HeartbeatFixedFields { sequence: 7, timestamp: 0 })
.encoded_length_with_header();
let dec = HeartbeatDecoder::try_from(&buf[..len])?;
assert_eq!(dec.sequence(), 7);
}
(From book/examples/heartbeat-encode.rs — compiled against the feature-tour codec.)
Staged chaining vs fixed-only
For a fixed-only message like Heartbeat, wrap* returns
HeartbeatEncoder<'_, H, FieldsUnfixed>. .fixed(&HeartbeatFixedFields { … })
consumes that value and returns FieldsFixed, which is the only phase that
exposes as_bytes_with_header / as_body_bytes / encoded_length* /
into_remaining_mut. Individual field setters stay
on the unfixed phase and on raw_fixed(); they are not on
that complete view.
Avoid (interrupted chain, rebinding):
// Each `let` breaks the chain and splays the pipeline across the screen.
// The `.unwrap()` calls are a code smell — the fallible chain should use `?`.
let enc = CarEncoder::wrap_and_apply_header(&mut buf, 0).fixed(&fields);
let enc = enc.fuel_figures(2, |g| { ... }).unwrap();
let enc = enc.manufacturer_as_str("Honda").unwrap();
let len = enc.encoded_length_with_header();
Every encoder stage is chainable — fixed() and each tail method return
the next stage (or Result<NextStage, _>) and compose with ? in the same
expression. Intermediate encoder rebinding and manual .unwrap() defeat this
design.
For the full Car example with groups and var-data, see the feature tour page.
Decoding
Sequential decode consumes each tail in wire order. For callbacks throughout
the chain, start with decoder.ordered():
#![allow(unused)]
fn main() {
pub fn demo_car_ordered_lane(wire: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
let mut speeds = Vec::new();
let mut octanes = Vec::new();
let mut text = Vec::new();
let len = CarDecoder::try_decode(wire, 0)?
.ordered()
.fixed(|car| {
assert_eq!(car.serial_number(), 1234);
Ok(())
})?
.fuel_figures(|entry, info| {
// `info` carries the position the wire declares.
assert_eq!(info.count, 2);
assert_eq!(info.is_last(), info.index == 1);
speeds.push(entry.speed());
Ok(entry.ordered().usage_description(|_| Ok(()))?)
})?
.performance_figures(|entry, _info| {
octanes.push(entry.octane_rating());
Ok(entry.ordered().acceleration(|a, _| {
let _ = (a.mph(), a.seconds());
Ok(())
})?)
})?
.manufacturer_as_str(|s| {
text.push(s.to_owned());
Ok(())
})?
.model_as_str(|s| {
text.push(s.to_owned());
Ok(())
})?
.activation_code_as_str(|s| {
text.push(s.to_owned());
Ok(())
})?
.encoded_length_with_header();
assert_eq!(speeds, vec![30, 60]);
assert_eq!(octanes, vec![95]);
assert_eq!(text, vec!["Honda", "Civic VTi", "abcdef"]);
assert_eq!(len, wire.len());
Ok(())
}
}
The fixed callback comes before tails and receives a fixed-fields-only view;
each group callback receives the entry and its EntryInfo, and var-data
callbacks receive bytes or strictly validated text. Every callback returns
Result<_, DecodeError>, so the closures infer. These
callbacks borrow the original wire buffer, so bytes and text may
be retained as the chain advances. The message chain ends on
encoded_length_with_header() / as_bytes_with_header(), like encode —
.done() is only to unwrap the staged complete. A nested ordered() walk
returns that completion to the parent callback directly. The
L3 sample
shows this recursive form.
The underlying staged API also supports explicit iteration and skipping. Its group and var-data spellings can require iterator and tuple bindings:
| Group entries | into_<group> gives you | Why |
|---|---|---|
| Carry their own groups or var-data | a visit closure, returning the entry’s completion | no stride — the completion is where the next entry starts |
| Fixed-stride (no tails of their own) | an iterator, for e in &mut iter | the next entry is offset + block length, so nothing has to be measured |
Var-data into_<name>() returns (payload, next). Text fields also offer
into_<name>_as_str(), with strict validation and the same tuple shape.
These borrowed payloads can be retained while advancing later stages.
Prefer:
#![allow(unused)]
fn main() {
pub fn demo_car_decode_stages(wire: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
let car = CarDecoder::try_decode(wire, 0)?;
assert_eq!(car.serial_number(), 1234);
assert_eq!(car.model_year(), 2013);
// Domain conversion: BooleanType → bool when configured.
let available: bool = car.available()?;
assert!(available);
assert_eq!(car.code(), Model::A);
assert_eq!(car.discounted_model(), Model::C); // constant field
assert_eq!(car.engine().capacity(), 2000);
// Schema order: groups then var-data, one chain. Both of Car's groups have
// entries that carry their own tails, so both take a visit closure: the
// completion the closure returns is where the next entry starts, so the
// group is walked exactly once.
let mut speeds = Vec::new();
let mut octanes = Vec::new();
let (mfr, decoder) = car
.into_fuel_figures(|entry| -> Result<_, sbe_rt::DecodeError> {
speeds.push(entry.speed());
// usageDescription is this entry's only tail, so reading it
// completes the entry.
entry.into_usage_description().map(|(_usage, done)| done)
})?
.into_performance_figures(|entry| -> Result<_, sbe_rt::DecodeError> {
octanes.push(entry.octane_rating());
// acceleration has fixed-stride entries, so it is a real iterator.
let mut accel = entry.into_acceleration()?;
assert_eq!((&mut accel).len(), 2);
accel.finish()
})?
.into_manufacturer_as_str()?;
assert_eq!(speeds, vec![30, 60]);
assert_eq!(octanes, vec![95]);
let (model, decoder) = decoder.into_model_as_str()?;
let (code, _decoder) = decoder.into_activation_code_as_str()?;
// All three &str coexist — each borrows 'a from the original wire buffer.
assert_eq!((mfr, model, code), ("Honda", "Civic VTi", "abcdef"));
Ok(())
}
}
(Real code from the sbe-feature-tour sample. fuelFigures and
performanceFigures entries carry tails, so they take closures;
acceleration is fixed-stride, so it is an iterator.)
Consecutive var-data fields chain through the next stage each tuple
returns; the borrowed slices stay valid while later stages advance:
#![allow(unused)]
fn main() {
fn count_car_text_bytes(wire: &[u8]) -> Result<usize, sbe_rt::DecodeError> {
let (manufacturer, next) = CarDecoder::try_from(wire)?
.skip_fuel_figures()?
.skip_performance_figures()?
.into_manufacturer()?;
let (model, next) = next.into_model()?;
let (activation_code, _complete) = next.into_activation_code()?;
Ok(manufacturer.len() + model.len() + activation_code.len())
}
}
For callbacks instead of tuples, use the ordered lane’s <name>(|bytes| ..)
and <name>_as_str(|text| ..), which return DecodeError.
The generated staged walk advances once through dynamic tails. Random-access getters used before that walk, including counts or lengths for later tails, can add scans. See Asking before walking.