with_conversion vs with_domain_type
Do not call both for the same selector — domain type already enables conversion.
A with_conversion | B with_domain_type | |
|---|---|---|
| Idea | Generic convert API; you plug any app type | Always use this Rust path |
| build.rs | .with_conversion(named_type("Decimal")) | .with_domain_type(…, "rust_decimal::Decimal") |
| You write | TryFromSbe<Decimal> / TryToSbe<Decimal> for your type | Usually nothing for bool / rust_decimal / chrono |
| Decode | let p: Cents = dec.price_as()? | let p: rust_decimal::Decimal = dec.try_price()? |
| Encode | enc.price_from(¢s)? | enc.try_price(rust_decimal::Decimal::new(12345, 2))? |
| Raw wire | price_value() / price_wire(...) | same when conversion is active |
| Sample | exchange-example · demo_conversion_only | l3-book |
Option A — you choose the app type (Cents)
#![allow(unused)] fn main() { use ergo_sbe::{ConversionSelector, GenerationConfig}; // A — generic converter: one wire type, many app types let _cfg = GenerationConfig::new("msgs").with_conversion(ConversionSelector::named_type("Decimal")); }
(From book/examples/conversion-config.rs — a self-contained program that compiles against ergo-sbe.)
#![allow(unused)] fn main() { use rust_decimal::Decimal as Rd; // App adapter: wire Decimal ↔ rust_decimal::Decimal struct FixedPrice { mantissa: i64, exponent: i8 } impl TryFromSbe<Decimal> for FixedPrice { type Error = &'static str; fn try_from_sbe(wire: Decimal) -> Result<Self, Self::Error> { Ok(FixedPrice { mantissa: wire.mantissa(), exponent: wire.exponent(), }) } } impl TryToSbe<Decimal> for FixedPrice { type Error = &'static str; fn try_to_sbe(&self) -> Result<Decimal, Self::Error> { Ok(Decimal::new(self.mantissa, self.exponent)) } } }
(From book/examples/conversion-app-code.rs — app adapter pattern, compiles against tour_codec.)
// Encode using the generic conversion API:
let mut buf = [0u8; QuoteEncoder::compute_length_with_header()];
let price = Rd::new(12345, 2); // 123.45
let len = QuoteEncoder::try_wrap_and_apply_header(&mut buf, 0)?
.price_from(&price)?
.size_from(&Rd::new(10, 0))?
.encoded_length_with_header();
// Decode — generic `_as::<T>()` picks your adapter:
let dec = QuoteDecoder::try_from(&buf[..len])?;
let p: Rd = dec.price_as()?;
assert_eq!(p, Rd::new(12345, 2));
// Same buffer, different app type — only possible with with_conversion:
let fixed: FixedPrice = dec.price_as()?;
assert_eq!(fixed.mantissa, 12345);
assert_eq!(fixed.exponent, -2);
(Same file — generic _from/_as encode/decode with with_conversion.)
Option B — one fixed app type
#![allow(unused)] fn main() { use ergo_sbe::{ConversionSelector, GenerationConfig}; // B — concrete mapping: one Rust type per wire type (already enables conversion) let _cfg = GenerationConfig::new("msgs").with_domain_type( ConversionSelector::named_type("Decimal"), "rust_decimal::Decimal", ); }
(Same source file — book/examples/conversion-config.rs.)
// Encode using the generic conversion API:
let mut buf = [0u8; QuoteEncoder::compute_length_with_header()];
let price = Rd::new(12345, 2); // 123.45
let len = QuoteEncoder::try_wrap_and_apply_header(&mut buf, 0)?
.price_from(&price)?
.size_from(&Rd::new(10, 0))?
.encoded_length_with_header();
// Decode — generic `_as::<T>()` picks your adapter:
let dec = QuoteDecoder::try_from(&buf[..len])?;
let p: Rd = dec.price_as()?;
assert_eq!(p, Rd::new(12345, 2));
// Same buffer, different app type — only possible with with_conversion:
let fixed: FixedPrice = dec.price_as()?;
assert_eq!(fixed.mantissa, 12345);
assert_eq!(fixed.exponent, -2);
Both styles on different fields:
pub fn demo_conversion_only() -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut buf = [0u8; QuoteEncoder::compute_length_with_header()];
let price = Rd::new(12345, 2); // 123.45
let size = Rd::new(10, 0);
let mut enc = QuoteEncoder::try_wrap_and_apply_header(&mut buf, 0)?;
enc.price_from(&price)?;
enc.size_from(&size)?;
let len = QuoteEncoder::compute_length_with_header();
let dec = QuoteDecoder::try_from(&buf[..len])?;
let wire = dec.price_value();
assert_eq!(wire.mantissa(), 12345);
assert_eq!(wire.exponent(), -2);
let price2: Rd = dec.price_as()?;
let size2: Rd = dec.size_as()?;
assert_eq!(price2, price);
assert_eq!(size2, size);
// Same buffer, different app type — only possible with with_conversion.
let fixed: FixedPrice = dec.price_as()?;
assert_eq!(
fixed,
FixedPrice {
mantissa: 12345,
exponent: -2
}
);
let dto = QuoteDomain::try_from_decoder(dec)?;
assert_eq!(dto.price.mantissa(), 12345);
let mut re = [0u8; QuoteEncoder::compute_length_with_header()];
let n = dto.encode(&mut re)?;
assert_eq!(&re[..n], &buf[..len]);
Ok(buf[..len].to_vec())
}
(This code comes from the sbe-feature-tour sample crate.)
Option B, manual impl — concrete signatures, your own conversion logic
with_domain_type(selector, path) is the common case: ergo-sbe also generates
the TryFromSbe/TryToSbe impl for bool / rust_decimal::Decimal /
chrono::DateTime<Utc>. If you need different conversion behaviour for one of
those exact three types — a custom rounding rule, stricter validation,
different null handling — call additive with_manual_domain_type
instead: same generated try_price(...)? / try_price()? signatures, but you
write the impl:
let (_schema, src) = generate_domain_with(&path, "manual_impl_dt", |c| {
c.with_manual_domain_type(
ergo_sbe::ConversionSelector::named_type("Decimal"),
"rust_decimal::Decimal",
)
});
use rust_decimal::Decimal;
// Caller-supplied impl: deliberately scales mantissa by 10 on the way
// in/out, so a value only round-trips correctly through THIS impl —
// proof ergo-sbe didn't quietly generate its own.
impl TryFromSbe<self::Decimal> for rust_decimal::Decimal {
type Error = &'static str;
fn try_from_sbe(wire: self::Decimal) -> Result<Self, Self::Error> {
Ok(rust_decimal::Decimal::new(wire.mantissa() / 10, (-wire.exponent()) as u32))
}
}
impl TryToSbe<self::Decimal> for rust_decimal::Decimal {
type Error = &'static str;
fn try_to_sbe(&self) -> Result<self::Decimal, Self::Error> {
Ok(self::Decimal::new(self.mantissa() as i64 * 10, -(self.scale() as i8)))
}
}
let mut buf = [0u8; 256];
let mut enc = OrderEncoder::wrap_and_apply_header(&mut buf, 0)
.fixed(&OrderFixedFields { price: self::Decimal::new(0, 0), size: self::Decimal::new(0, 0) });
enc.try_price(Decimal::new(12345, 2))?;
enc.try_size(Decimal::new(100, 0))?;
let encoded = enc.as_bytes_with_header().to_vec();
let dec = OrderDecoder::try_decode(&encoded, 0)?;
assert_eq!(dec.try_price()?, Decimal::new(12345, 2));
assert_eq!(dec.try_size()?, Decimal::new(100, 0));
(From domain_type_manual_impl_uses_callers_own_impl in
sbe/tests/baseline_test.rs — a real generated-and-compiled test. Any
rust_type string that isn't one of the three built-ins never gets an
auto-generated impl regardless of DomainImpl — it only matters for opting
those three in or out.)
Forgot the impl? Two things soften it. First, the compile error names the missing impl directly instead of the default trait-bound message:
error[E0277]: `rust_decimal::Decimal` has no `TryFromSbe<Decimal>` impl
|
| missing `impl TryFromSbe<Decimal> for rust_decimal::Decimal`
|
= note: if this field uses DomainImpl::Manual, the generated `try_*`
accessor's doc comment has a ready-to-paste starting point
Second, for the three built-ins, that pointer is real: the generated
try_price method's own doc comment (visible on hover, or in cargo doc)
carries the exact impl DomainImpl::Generated would have written — copy it
out and adjust. sbe/tests/baseline_test.rs's
domain_type_manual_impl_doc_comment_has_generated_snippet asserts this
snippet is present in the generated source.