Skip to main content

ergo_sbe/
lib.rs

1#![warn(missing_docs)]
2// Crate-root allows are deliberately few. Codegen-specific noise lives on
3// `codegen` (and other modules) via scoped attributes — not a 40-line blanket
4// silence of workspace lint policy.
5//
6// Justified at crate root (schema/codegen reality, not laziness):
7#![allow(clippy::too_many_arguments)] // Generator pipelines thread many schema/config params
8#![allow(clippy::too_many_lines)] // Emit functions are inherently large token builders
9#![allow(clippy::doc_markdown)] // SBE identifiers (blockLength, templateId) trip false positives
10#![allow(clippy::cast_possible_truncation)] // Numeric widths constrained by schema validation
11#![allow(clippy::cast_sign_loss)] // Same: ranges validated against primitive types
12
13//! Opinionated, idiomatic Rust code generation for Simple Binary Encoding (SBE).
14//!
15//! [Simple Binary Encoding][sbe-spec] describes messages in XML; ergo-sbe
16//! parses those schemas and emits safe, version-aware Rust codecs for
17//! low-latency trading.
18//!
19//! # Documentation
20//!
21//! - **[ergo-sbe book](https://mimran1980.github.io/ergon/)** — getting started,
22//!   feature tour, core concepts, configuration, recipes, design notes
23//! - [Getting started](https://mimran1980.github.io/ergon/sbe/getting-started.html) ·
24//!   [Feature tour](https://mimran1980.github.io/ergon/sbe/feature-tour.html) ·
25//!   [Coming from sbe-tool](https://mimran1980.github.io/ergon/sbe/getting-started/from-sbe-tool.html) ·
26//!   [Type-state design](https://mimran1980.github.io/ergon/sbe/design-notes/type-state.html)
27//! - [Crate README](https://github.com/mimran1980/ergon/blob/main/sbe/README.md)
28//!
29//! ## In a few words
30//!
31//! - **Compile-time wire order** — calling `asks` before `bids` is a type error
32//! - **Closure-based groups** — nested shape mirrors the schema, no `.parent()` hopscotch
33//! - **Exact buffer sizing** — no oversize scratch buffers; works directly with
34//!   Aeron `try_claim`
35//! - **Three-tier trust boundary** — `try_*` constructors validate the buffer
36//!   extent and return `Result`; bare `wrap` / `wrap_and_apply_header` /
37//!   `decode` prove the same extent and **panic** if short; `unsafe fn
38//!   *_unchecked` skips checks (caller proves the extent in `# Safety`)
39//! - **Zero heap allocation** on generated hot paths; zero runtime dependencies
40//! - **Domain types** — map wire `Decimal` to `rust_decimal::Decimal` with one
41//!   line of config
42//!
43//! # Architecture
44//!
45//! | Layer | Module | Responsibility |
46//! |-------|--------|----------------|
47//! | Schema input | [`xml`], [`xsd`], [`schema`] | Parse SBE XML, optional XSD shape check, [`Schema`] |
48//! | Intermediate | [`ir`], [`resolve`] | Token stream + offsets / block lengths |
49//! | Options | [`config`] | Module name, conversions, domain objects, … |
50//! | Codegen | [`codegen`] | Rust source modules |
51//!
52//! # Quick-start (`build.rs`) — doctest pins generated idioms
53//!
54//! Generated *application* types do not exist in this crate at doctest time.
55//! The example below **runs the generator** on an inline schema and asserts on
56//! the emitted source so chained encode / length-builder names cannot drift
57//! unnoticed. For end-to-end encode/decode of real types, see
58//! [`samples/sbe-feature-tour`](https://github.com/mimran1980/ergon/tree/main/samples/sbe-feature-tour)
59//! and the golden file
60//! [`sbe/tests/golden/car_example.rs`](https://github.com/mimran1980/ergon/blob/main/sbe/tests/golden/car_example.rs).
61//!
62//! ```rust
63//! use ergo_sbe::{parse, Generator, GenerationConfig, Schema};
64//!
65//! let schema_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
66//! <messageSchema package="example.sbe" id="1" version="0"
67//!                byteOrder="littleEndian">
68//!   <types>
69//!     <composite name="messageHeader">
70//!       <type name="blockLength" primitiveType="uint16"/>
71//!       <type name="templateId"   primitiveType="uint16"/>
72//!       <type name="schemaId"     primitiveType="uint16"/>
73//!       <type name="version"      primitiveType="uint16"/>
74//!     </composite>
75//!   </types>
76//!   <message name="Car" id="1">
77//!     <field name="serialNumber" id="1" type="uint64" offset="0"/>
78//!     <field name="modelYear"    id="2" type="uint16" offset="8"/>
79//!   </message>
80//! </messageSchema>"#;
81//!
82//! let ir = parse(schema_xml).expect("parse schema");
83//! let schema = Schema::from_ir(ir);
84//! let output = Generator::new(GenerationConfig::new("my_messages"))
85//!     .generate(&schema)
86//!     .expect("generate");
87//! let src = &output.modules().next().expect("one module").source;
88//! assert!(src.contains("CarDecoder"));
89//! assert!(src.contains("CarEncoder"));
90//! assert!(src.contains("CarFixedFields"));
91//! assert!(src.contains("wrap_and_apply_header"));
92//! assert!(src.contains("compute_length_with_header") || src.contains("ENCODED_LENGTH"));
93//! // write module.source to OUT_DIR and `include!` it from your crate
94//! ```
95//!
96//! # What gets generated (how to use it)
97//!
98//! Names depend on your schema (`Car` below is illustrative). Prefer the
99//! feature-tour sample and golden file over prose-only snippets.
100//!
101//! ## Composites = wire images (not `repr(C)` overlays)
102//!
103//! Generated composites are `#[repr(transparent)] struct Engine(pub [u8; N])`:
104//! the value **is** the on-wire byte block. Accessors use explicit
105//! `from_le_bytes` / `to_le_bytes` at schema offsets (portable; free on LE).
106//! Default decode is a flyweight (`EngineDecoder { buf, pos }`) — zero-copy
107//! into the message. Do **not** transmute the buffer to a padded `#[repr(C)]`
108//! field struct: SBE is packed and may be unaligned. See the crate README
109//! section *Composite layout & little-endian*.
110//!
111//! ## Decode flyweight
112//!
113//! → [`samples/sbe-feature-tour`](https://github.com/mimran1980/ergon/blob/main/samples/sbe-feature-tour/src/lib.rs)
114//!
115//! ## Encode + type-state tails (buffer sizing)
116//!
117//! **Size the buffer first** with the staged `*EncodedLength` builder —
118//! never guess with a large `vec![0u8; 4096]`. For fixed-only messages
119//! use the const `ENCODED_LENGTH`.
120//!
121//! → [`samples/sbe-feature-tour`](https://github.com/mimran1980/ergon/blob/main/samples/sbe-feature-tour/src/lib.rs)
122//!
123//! ## Buffer sizing
124//!
125//! Schema-aware helpers size the buffer **before** you write — including
126//! nested groups and ragged var-data — so you do not hand-calculate wire length
127//! for complicated messages. Prefer a **stack array** when the length is
128//! `const` (`ENCODED_LENGTH` / `compute_encoded_length_*`); for runtime
129//! lengths, size first then claim or encode into an exact-length slot.
130//!
131//! | Shape | API | Prefer |
132//! |-------|-----|--------|
133//! | Fixed-only | `HeartbeatEncoder::ENCODED_LENGTH` (const) | `[0u8; N]` stack |
134//! | Flat / known tails | `compute_encoded_length_with_message_header(...)` | stack when const |
135//! | Groups / nested / ragged | `CarEncodedLength::new()…encoded_length_with_header()` | exact claim / slot |
136//!
137//! ## Why wire order is compile-time
138//!
139//! SBE is positional. Two adjacent identical groups (e.g. bids then asks) are
140//! common in market data; swapping them still produces “valid” bytes and only
141//! fails in production. Named stage structs make the wrong order a type error.
142//!
143//! ## Field metadata (Java parity)
144//!
145//! → [`sbe/tests/java_parity_features_test.rs`](https://github.com/mimran1980/ergon/blob/main/sbe/tests/java_parity_features_test.rs)
146//!
147//! ## Conversion: `with_conversion` vs `with_domain_type`
148//!
149//! **Pick one style per selector** — `with_domain_type` already enables conversion.
150//!
151//! | Config | Generated decode | Generated encode |
152//! |--------|------------------|------------------|
153//! | [`GenerationConfig::with_conversion`] | `dec.price_as::<T>()?` | `enc.price_from(&t)?` |
154//! | [`GenerationConfig::with_domain_type`] | `dec.try_price()? -> path::Type` | `enc.try_price(value)?` |
155//!
156//! ```rust
157//! use ergo_sbe::{ConversionSelector, GenerationConfig};
158//!
159//! // A — pluggable: you implement TryFromSbe / TryToSbe
160//! let _a = GenerationConfig::new("msgs")
161//!     .with_conversion(ConversionSelector::named_type("Decimal"));
162//!
163//! // B — concrete Rust type (implies conversion for the same selector)
164//! let _b = GenerationConfig::new("msgs")
165//!     .with_domain_type(
166//!         ConversionSelector::named_type("Decimal"),
167//!         "rust_decimal::Decimal",
168//!     );
169//! ```
170//!
171//! See [`sbe/tests/comprehensive_test.rs`](https://github.com/mimran1980/ergon/blob/main/sbe/tests/comprehensive_test.rs)
172//! (conversion and domain-object coverage).
173//!
174//! ## Domain objects
175//!
176//! [`GenerationConfig::with_domain_objects`] with a [`DomainVarData`] mode emits
177//! owned structs. Use [`DomainVarData::Strings`] for text (`String`;
178//! invalid UTF-8 → `InvalidUtf8` error) or [`DomainVarData::Bytes`] for `Vec<u8>`.
179//!
180//! See [`sbe/tests/domain_objects_test.rs`](https://github.com/mimran1980/ergon/blob/main/sbe/tests/domain_objects_test.rs)
181//!
182//! ## Multi-message dispatch
183//!
184//! See [`sbe/fuzz/fuzz_targets/any_message_frame_cursor.rs`](https://github.com/mimran1980/ergon/blob/main/sbe/fuzz/fuzz_targets/any_message_frame_cursor.rs)
185//!
186//! ## Fixed arrays / char fields
187//!
188//! See [`sbe/tests/java_parity_features_test.rs`](https://github.com/mimran1980/ergon/blob/main/sbe/tests/java_parity_features_test.rs)
189//!
190//! ## Keywords in schema names
191//!
192//! Field `type` becomes `type_` (default append `"_"`). Override with
193//! [`GenerationConfig::with_keyword_append_token`].
194//!
195//! ## XSD structural check
196//!
197//! Optional CI gate: [`validate_against_sbe_xsd`] or [`parse_with_xsd_validation`].
198//! Official XSD text is embedded as [`SBE_XSD`].
199//!
200//! Note this is a *shape* check, not a full W3C engine, and it is opt-in.
201//! [`parse`] itself always rejects malformed XML, a bad root, unexpected
202//! elements, and unknown attributes.
203//!
204//! # Design
205//!
206//! - Wire-compatible with official SBE / sbe-tool baselines where tested
207//! - Idiomatic Rust (type-state tails, borrow flyweights) — not a Java port
208//! - Zero allocation on decode hot paths by default
209//! - Version-aware accessors (`sinceVersion` / acting version)
210//! - Unsafe only on explicit `_unchecked` / documented paths
211//!
212//! Benchmarks: [benchmarks chapter](https://mimran1980.github.io/ergon/sbe/benchmarks.html).
213//!
214//! [sbe-spec]: https://www.fixtrading.org/standards/sbe/
215
216/// Re-exported so `build.rs` can return [`miette::Result`] without an extra
217/// dependency. Enable the crate's `fancy` feature for graphical rendering
218/// (source snippet + span) instead of the plain fallback.
219pub use miette;
220
221/// Cargo `build.rs` helpers ([`generate_to_out_dir`], [`sbe_mod!`]).
222#[allow(
223    clippy::pedantic,
224    clippy::nursery,
225    clippy::unwrap_used,
226    clippy::expect_used,
227    clippy::result_large_err
228)]
229pub mod build;
230/// Codec generation ([`Generator`]).
231// Scoped: quote!/token emit paths and submodule re-exports trip style lints and
232// unused_import on `pub(crate) use` hubs. Prefer fixing real dead code over
233// growing this list — do not re-blanket the crate root.
234#[allow(
235    unused,
236    clippy::pedantic,
237    clippy::nursery,
238    clippy::unwrap_used,
239    clippy::expect_used,
240    clippy::panic,
241    clippy::manual_memcpy,
242    clippy::needless_range_loop,
243    clippy::unnecessary_cast,
244    clippy::useless_format,
245    clippy::items_after_statements,
246    clippy::explicit_counter_loop,
247    clippy::uninlined_format_args,
248    clippy::collapsible_if,
249    clippy::unreadable_literal,
250    clippy::match_same_arms,
251    clippy::needless_borrow,
252    clippy::use_self,
253    clippy::missing_const_for_fn,
254    clippy::result_large_err,
255    clippy::similar_names,
256    clippy::redundant_clone,
257    clippy::ref_option,
258    clippy::map_unwrap_or,
259    clippy::redundant_closure_for_method_calls,
260    clippy::unnecessary_unwrap,
261    clippy::cast_lossless,
262    clippy::cast_precision_loss,
263    clippy::if_same_then_else,
264    clippy::should_panic_without_expect,
265    clippy::module_name_repetitions,
266    clippy::option_if_let_else,
267    clippy::match_wildcard_for_single_variants,
268    clippy::single_match_else,
269    clippy::fn_params_excessive_bools,
270    clippy::cast_enum_constructor,
271    clippy::ptr_as_ptr,
272    // Recursive-descent group encoder helper; legitimate recursion.
273    clippy::only_used_in_recursion
274)]
275pub mod codegen;
276/// [`GenerationConfig`] — conversions, domain objects, keywords, etc.
277#[allow(
278    dead_code,
279    clippy::pedantic,
280    clippy::nursery,
281    clippy::unwrap_used,
282    clippy::expect_used
283)]
284pub mod config;
285/// Token [`Ir`] (usually via [`Schema`]).
286#[doc(hidden)]
287#[allow(clippy::pedantic, clippy::nursery)]
288pub mod ir;
289/// Offset resolution ([`resolve_schema`]; called by parse).
290#[doc(hidden)]
291#[allow(
292    clippy::pedantic,
293    clippy::nursery,
294    clippy::unwrap_used,
295    clippy::expect_used,
296    clippy::cast_lossless,
297    clippy::cast_precision_loss,
298    clippy::result_large_err,
299    clippy::collapsible_if,
300    clippy::needless_range_loop
301)]
302pub mod resolve;
303/// [`Schema`] handle for codegen.
304#[allow(
305    dead_code,
306    clippy::pedantic,
307    clippy::nursery,
308    clippy::unnecessary_wraps
309)]
310pub mod schema;
311/// Structured IR for codegen (internal).
312mod schema_attrs;
313#[allow(
314    dead_code,
315    unused_imports,
316    unused_variables,
317    clippy::pedantic,
318    clippy::nursery,
319    clippy::cast_lossless
320)]
321pub(crate) mod structured_ir;
322/// XML parse ([`parse`], [`parse_file`]).
323#[allow(
324    unused,
325    dead_code,
326    clippy::pedantic,
327    clippy::nursery,
328    clippy::unwrap_used,
329    clippy::expect_used,
330    clippy::panic,
331    clippy::cast_lossless,
332    clippy::cast_precision_loss,
333    clippy::manual_memcpy,
334    clippy::needless_range_loop,
335    clippy::collapsible_if,
336    clippy::match_same_arms,
337    clippy::similar_names,
338    clippy::redundant_clone,
339    clippy::option_if_let_else,
340    clippy::module_name_repetitions,
341    clippy::items_after_statements,
342    clippy::uninlined_format_args,
343    clippy::result_large_err,
344    clippy::unnecessary_cast
345)]
346pub mod xml;
347/// Optional XSD-shaped validation ([`validate_against_sbe_xsd`], [`SBE_XSD`]).
348#[allow(
349    clippy::pedantic,
350    clippy::nursery,
351    clippy::unwrap_used,
352    clippy::expect_used
353)]
354pub mod xsd;
355
356pub use build::{
357    BuildError, SchemaFile, generate_multi_to_dir, generate_multi_to_out_dir, generate_str_to_dir,
358    generate_str_to_out_dir, generate_to_dir, generate_to_out_dir, out_dir,
359};
360pub use codegen::{GenerateError, GeneratedModule, GeneratedModuleSet, Generator};
361pub use config::{
362    ConversionSelector, DomainImpl, DomainVarData, EnumVariantInfo, FieldInfo, GenerationConfig,
363    GenerationProfile, ItemContext, ItemKind, SetChoiceInfo,
364};
365pub use ir::{ByteOrder, Encoding, Ir, Presence, PrimitiveType, Signal, Token};
366pub use resolve::{ResolveError, resolve_schema};
367pub use schema::Schema;
368pub use xml::{
369    IncludeCause, ParseError, parse, parse_file, parse_file_with_shared, parse_with_shared,
370    parse_with_xsd_validation,
371};
372pub use xsd::{SBE_XSD, XsdValidationError, validate_against_sbe_xsd};
373
374/// Chrono timestamp converters — feature-gated behind `chrono`.
375///
376/// Use [`GenerationConfig::with_domain_type`] with
377/// `"chrono::DateTime<chrono::Utc>"` or `"chrono::NaiveDateTime"` to
378/// generate `try_*` / `try_set_*` methods that convert between SBE `i64`
379/// wire values and chrono datetime types.
380#[cfg(feature = "chrono")]
381pub mod chrono_converters;
382
383// Re-export optional dependencies so generated codecs can name the types
384// without the consumer adding them directly. Feature-gated codec methods
385// (into_<field>_as_compact_str, etc.) use these paths.
386#[cfg(feature = "bytes")]
387pub use bytes;
388#[cfg(feature = "compact_str")]
389pub use compact_str;
390#[cfg(feature = "smol_str")]
391pub use smol_str;
392
393// Header-state markers (`HeaderPresent` / `HeaderAbsent`) live in each
394// generated module's `sbe_rt` (see `generate_sbe_rt_src`). They are not
395// re-exported here: generated codecs seal against their own `sbe_rt::HeaderState`,
396// so a shared `ergo_sbe::header_state` type would not unify with `H`.