ergo_sbe/config.rs
1//! Code generation configuration ([`GenerationConfig`]).
2//!
3//! # Conversion: pick **one** style per selector
4//!
5//! | API | When to use | Generated decode | Generated encode |
6//! |-----|-------------|------------------|------------------|
7//! | [`GenerationConfig::with_conversion`] | Pluggable adapters; no forced crate dep | `dec.price_as::<T>()?` | `enc.price_from(&t)?` |
8//! | [`GenerationConfig::with_domain_type`] | One canonical app type | `dec.try_price()? -> path::Type` | `enc.try_price(value)?` |
9//!
10//! `with_domain_type` **implies** conversion for that selector. Do **not** also
11//! call `with_conversion` for the same selector.
12//!
13//! ```rust
14//! use ergo_sbe::{GenerationConfig, ConversionSelector};
15//!
16//! // A — generic / pluggable (you implement TryFromSbe / TryToSbe)
17//! let _a = GenerationConfig::new("msgs")
18//! .with_conversion(ConversionSelector::named_type("Decimal"));
19//!
20//! // B — concrete Rust type (Generated impl is the default)
21//! let _b = GenerationConfig::new("msgs")
22//! .with_domain_type(
23//! ConversionSelector::named_type("Decimal"),
24//! "rust_decimal::Decimal",
25//! );
26//! ```
27//!
28//! # Other features (generated surface)
29//!
30//! | Builder | What generated code looks like |
31//! |---------|--------------------------------|
32//! | [`with_domain_objects`](GenerationConfig::with_domain_objects) | `CarDomain` DTOs; pass [`DomainVarData`] for var-data shape |
33//! | [`with_shared_module`](GenerationConfig::with_shared_module) | Multi-schema: shared types in one module, `pub use super::common::*` |
34//! | [`with_external_sbe_rt`](GenerationConfig::with_external_sbe_rt) | `pub use path::sbe_rt as sbe_rt` instead of inlining runtime |
35//! | [`with_error_from_impls`](GenerationConfig::with_error_from_impls) | `From<EncodeError> for YourError` so `?` works |
36//! | [`with_keyword_append_token`](GenerationConfig::with_keyword_append_token) | Schema field `type` → `type_` (default `"_"`) |
37//! | [`with_deprecated_attrs`](GenerationConfig::with_deprecated_attrs) | `#[deprecated]` on schema-deprecated items |
38
39/// Selects which fields receive conversion / domain-type methods.
40///
41/// When several selectors could match the same field, precedence is:
42/// 1. Exact `"Message.field"` path ([`ConversionSelector::FieldPath`])
43/// 2. SBE `semanticType` ([`ConversionSelector::SemanticType`])
44/// 3. Named type ([`ConversionSelector::NamedType`])
45///
46/// ```rust
47/// use ergo_sbe::ConversionSelector;
48///
49/// let _ = ConversionSelector::named_type("Decimal");
50/// let _ = ConversionSelector::semantic_type("UTCTimestamp");
51/// let _ = ConversionSelector::field_path("Quote.price");
52/// ```
53#[derive(Clone, Debug, Eq, PartialEq, Hash)]
54pub enum ConversionSelector {
55 /// Match one field by path, e.g. `"Car.serialNumber"` or `"Quote.price"`.
56 FieldPath(String),
57 /// Match all fields with this SBE `semanticType` attribute (e.g. `"UTCTimestamp"`).
58 SemanticType(String),
59 /// Match all fields whose type name is this (composite, enum, set, alias).
60 ///
61 /// Example: `"Decimal"` matches every field of composite type `Decimal`.
62 NamedType(String),
63}
64
65impl ConversionSelector {
66 /// Select by SBE `semanticType` (e.g. `"UTCTimestamp"`, `"Price"`).
67 #[must_use]
68 pub fn semantic_type(name: impl Into<String>) -> Self {
69 Self::SemanticType(name.into())
70 }
71
72 /// Select by named SBE type (composite / enum / set / alias), e.g. `"Decimal"`.
73 #[must_use]
74 pub fn named_type(name: impl Into<String>) -> Self {
75 Self::NamedType(name.into())
76 }
77
78 /// Select by exact `"MessageName.fieldName"` path.
79 #[must_use]
80 pub fn field_path(path: impl Into<String>) -> Self {
81 Self::FieldPath(path.into())
82 }
83}
84
85/// How owned domain DTO `<data>` / var-data fields are typed.
86///
87/// Passed to [`GenerationConfig::with_domain_objects`]. Wire is always
88/// length-prefixed **bytes**; this only chooses the **owned** DTO field type.
89///
90/// | Variant | DTO field | Feature | Invalid UTF-8 |
91/// |---------|-----------|---------|---------------|
92/// | [`Bytes`](DomainVarData::Bytes) | `Vec<u8>` | — | n/a |
93/// | [`Strings`](DomainVarData::Strings) | `String` | — | **`InvalidUtf8` error** |
94/// | `CompactStrings` | `CompactString` | `compact_str` | **`InvalidUtf8` error** |
95/// | `SmolStrings` | `SmolStr` | `smol_str` | **`InvalidUtf8` error** |
96/// | `BytesCrate` | `bytes::Bytes` | `bytes` | n/a |
97#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
98pub enum DomainVarData {
99 /// Byte-exact var-data (`Vec<u8>`) — binary tails or lossless re-encode.
100 #[default]
101 Bytes,
102 /// Text-friendly var-data (`String`). Invalid UTF-8 returns
103 /// `DecodeError::InvalidUtf8` (strict).
104 Strings,
105 /// Inline short strings (`compact_str::CompactString`, ≤24 bytes on stack).
106 /// Requires feature `compact_str`.
107 #[cfg(feature = "compact_str")]
108 CompactStrings,
109 /// O(1)-clone strings (`smol_str::SmolStr`). Requires feature `smol_str`.
110 #[cfg(feature = "smol_str")]
111 SmolStrings,
112 /// Zero-copy shared buffer (`bytes::Bytes`). Requires feature `bytes`.
113 #[cfg(feature = "bytes")]
114 BytesCrate,
115}
116
117/// Who writes the `TryFromSbe`/`TryToSbe` impl for a built-in
118/// [`GenerationConfig::with_domain_type`] mapping (`bool` /
119/// `rust_decimal::Decimal` / `chrono::DateTime<Utc>`).
120///
121/// Only matters for those three well-known type paths — any other
122/// `rust_type` never gets an auto-generated impl regardless of this setting,
123/// since ergo-sbe has no built-in conversion logic to offer for it.
124#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
125pub enum DomainImpl {
126 /// ergo-sbe writes the impl for you (default).
127 #[default]
128 Generated,
129 /// You write `impl TryFromSbe<Wire>` / `impl TryToSbe<Wire>` yourself —
130 /// e.g. a custom rounding rule, or null/validation behaviour the
131 /// built-in impl doesn't match. ergo-sbe still generates the concrete
132 /// `try_price(...)?` / `try_price()?` signatures that call it.
133 Manual,
134}
135
136/// Generated-code surface presets.
137///
138/// Individual knobs (`with_display_debug`, …) still override after
139/// [`GenerationConfig::profile`].
140#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
141pub enum GenerationProfile {
142 /// Display/Debug, field meta, multi-template dispatch, and all conveniences
143 /// that the boolean knobs enable by default.
144 #[default]
145 Full,
146 /// Byte codec + typed stages + exact sizing only. Omits Display/Debug,
147 /// meta-attribute constants, and `AnyMessage`/`FrameCursor` dispatch.
148 /// Domain DTOs and conversions stay off unless re-enabled explicitly.
149 Lean,
150}
151
152// ── Hook types ────────────────────────────────────────────────────────────
153
154/// Kinds of generated items a hook can observe.
155#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
156pub enum ItemKind {
157 /// An SBE enum type.
158 Enum,
159 /// An SBE bitset type.
160 Set,
161 /// An SBE composite type.
162 Composite,
163 /// A message decoder (flyweight over `&[u8]`).
164 MessageDecoder,
165 /// A message encoder (writes into `&mut [u8]`).
166 MessageEncoder,
167 /// A domain DTO struct.
168 DomainStruct,
169}
170
171/// One enum variant for hook introspection.
172#[derive(Clone, Debug)]
173pub struct EnumVariantInfo {
174 /// Variant name in PascalCase (e.g. "Ok", "Error").
175 pub name: String,
176 /// Variant name in snake_case (e.g. "ok", "error").
177 pub snake_name: String,
178 /// Raw name from the schema (e.g. "Ok", "hasPrice"). Use for serde labels.
179 pub label: String,
180 /// Wire discriminant value. Widened to `i128` so `uint64` discriminants
181 /// above `i64::MAX` are represented faithfully rather than wrapping negative.
182 pub value: i128,
183 /// Schema description, if present.
184 pub description: Option<String>,
185}
186
187/// One bitset choice for hook introspection.
188#[derive(Clone, Debug)]
189pub struct SetChoiceInfo {
190 /// Choice name in PascalCase (e.g. "HasPrice").
191 pub name: String,
192 /// Choice name in snake_case (e.g. "has_price"). Use for accessor calls.
193 pub snake_name: String,
194 /// Raw name from the schema (e.g. "hasPrice"). Use for serde labels.
195 pub label: String,
196 /// Zero-based bit position in the bitset.
197 pub bit_position: u8,
198 /// Schema description, if present.
199 pub description: Option<String>,
200}
201
202/// One field for hook introspection.
203#[derive(Clone, Debug)]
204pub struct FieldInfo {
205 /// Field name in snake_case.
206 pub name: String,
207 /// Rust type (e.g. "i64", "u8", "EventCode").
208 pub rust_type: String,
209 /// Byte offset from the message body start, when this is a fixed
210 /// scalar/array/composite/enum/set field. `None` for groups and
211 /// var-data fields, which have no single wire offset.
212 pub offset: Option<usize>,
213 /// Schema version this field was introduced in (0 = always present).
214 pub since_version: u16,
215 /// SBE `semanticType` attribute, if set.
216 pub semantic_type: Option<String>,
217 /// SBE presence: `"required"`, `"optional"`, or `"constant"`.
218 pub presence: &'static str,
219 /// Null sentinel value (optional fields only).
220 pub null_value: Option<u64>,
221 /// Whether the field is schema-deprecated.
222 pub deprecated: bool,
223 /// Schema description on the field, if present.
224 pub description: Option<String>,
225}
226
227/// Per-item context passed to hooks.
228///
229/// Every variant carries a `schema` reference for full IR access
230/// when the structured fields aren't enough.
231///
232/// Pattern-match on the variant to access item-specific data
233/// (variants, choices, fields). Use [`quote::quote!`] in your
234/// hook body to return tokens appended after the generated item.
235// manual Debug/Clone because &Schema in every variant makes derive unhappy
236#[derive(Clone)]
237pub enum ItemContext<'a> {
238 /// An SBE enum after codegen of its Rust type.
239 Enum {
240 /// Full schema IR for advanced hooks.
241 schema: &'a crate::Schema,
242 /// Enum type name (PascalCase).
243 name: String,
244 /// Wire encoding type (e.g. `"u8"`).
245 encoding_type: String,
246 /// Variants in schema order.
247 variants: Vec<EnumVariantInfo>,
248 },
249 /// An SBE bitset after codegen.
250 Set {
251 /// Full schema IR for advanced hooks.
252 schema: &'a crate::Schema,
253 /// Set type name (PascalCase).
254 name: String,
255 /// Wire encoding type (e.g. `"u8"`).
256 encoding_type: String,
257 /// Bit choices in schema order.
258 choices: Vec<SetChoiceInfo>,
259 },
260 /// An SBE composite after codegen.
261 Composite {
262 /// Full schema IR for advanced hooks.
263 schema: &'a crate::Schema,
264 /// Composite type name (PascalCase).
265 name: String,
266 /// Member fields in wire order.
267 fields: Vec<FieldInfo>,
268 },
269 /// A message decoder flyweight.
270 MessageDecoder {
271 /// Full schema IR for advanced hooks.
272 schema: &'a crate::Schema,
273 /// Message name (PascalCase).
274 name: String,
275 /// SBE template id.
276 template_id: u16,
277 /// Compiled fixed block length in bytes.
278 block_length: usize,
279 /// Fixed/group/data fields visible on this message.
280 fields: Vec<FieldInfo>,
281 },
282 /// A message encoder stage root.
283 MessageEncoder {
284 /// Full schema IR for advanced hooks.
285 schema: &'a crate::Schema,
286 /// Message name (PascalCase).
287 name: String,
288 /// SBE template id.
289 template_id: u16,
290 /// Compiled fixed block length in bytes.
291 block_length: usize,
292 /// Fixed/group/data fields visible on this message.
293 fields: Vec<FieldInfo>,
294 },
295 /// An owned domain DTO (`*Domain`) when domain objects are enabled.
296 DomainStruct {
297 /// Full schema IR for advanced hooks.
298 schema: &'a crate::Schema,
299 /// DTO type name (PascalCase).
300 name: String,
301 /// Fields on the DTO (including groups/var-data as owned types).
302 fields: Vec<FieldInfo>,
303 },
304}
305
306impl std::fmt::Debug for ItemContext<'_> {
307 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308 let (kind, name) = match self {
309 Self::Enum { name, .. } => ("Enum", name.as_str()),
310 Self::Set { name, .. } => ("Set", name.as_str()),
311 Self::Composite { name, .. } => ("Composite", name.as_str()),
312 Self::MessageDecoder { name, .. } => ("MessageDecoder", name.as_str()),
313 Self::MessageEncoder { name, .. } => ("MessageEncoder", name.as_str()),
314 Self::DomainStruct { name, .. } => ("DomainStruct", name.as_str()),
315 };
316 f.debug_struct("ItemContext")
317 .field("kind", &kind)
318 .field("name", &name)
319 .finish()
320 }
321}
322
323/// Token streams returned by hooks — appended after the generated item.
324pub type HookFn = dyn Fn(&ItemContext<'_>) -> Vec<proc_macro2::TokenStream> + Send + Sync;
325
326/// Wrapper so hooks can live in [`GenerationConfig`]. Uses [`Arc`] so
327/// `GenerationConfig` can derive [`Clone`].
328#[derive(Clone, Default)]
329pub(crate) struct Hooks(Vec<std::sync::Arc<HookFn>>);
330
331impl std::fmt::Debug for Hooks {
332 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333 f.debug_tuple("Hooks").field(&self.0.len()).finish()
334 }
335}
336impl Hooks {
337 pub(crate) fn push(&mut self, hook: std::sync::Arc<HookFn>) {
338 self.0.push(hook);
339 }
340 pub(crate) fn iter(&self) -> std::slice::Iter<'_, std::sync::Arc<HookFn>> {
341 self.0.iter()
342 }
343 pub(crate) fn is_empty(&self) -> bool {
344 self.0.is_empty()
345 }
346}
347
348// ── GenerationConfig ──────────────────────────────────────────────────────
349
350/// Options that shape generated Rust codecs.
351///
352/// Start with [`GenerationConfig::new`], chain builder methods, then pass to
353/// [`crate::Generator::new`].
354///
355/// ```rust
356/// use ergo_sbe::{DomainVarData, GenerationConfig, ConversionSelector};
357///
358/// let config = GenerationConfig::new("market_data")
359/// .with_domain_objects(DomainVarData::Strings)
360/// .with_domain_type(
361/// ConversionSelector::named_type("Decimal"),
362/// "rust_decimal::Decimal",
363/// );
364/// ```
365#[derive(Clone)]
366pub struct GenerationConfig {
367 /// Rust module name for the generated output file (`{module_name}.rs`).
368 pub(crate) module_name: String,
369 /// Sibling module that already owns shared types (multi-schema mode).
370 pub(crate) shared_module: Option<String>,
371 /// Emit owned `*Domain` structs + `TryFrom<&Decoder>` / `encode`.
372 pub(crate) domain_objects: bool,
373 /// Var-data shape on DTOs when `domain_objects` is set.
374 pub(crate) domain_var_data: DomainVarData,
375 /// Selectors for generic `*_as` / `*_from` conversion methods.
376 pub(crate) conversions: Vec<ConversionSelector>,
377 /// Domain-type mappings: `(selector, rust_type_path)`.
378 /// Implicitly enables conversion for the same selector.
379 pub(crate) domain_types: Vec<(ConversionSelector, String)>,
380 /// Selectors passed to [`Self::with_manual_domain_type`]:
381 /// get the same concrete `try_*` signatures as any other `domain_types`
382 /// entry, but the built-in `TryFromSbe`/`TryToSbe` impl (for `bool` /
383 /// `rust_decimal::Decimal` / `chrono::DateTime<Utc>`) is not generated —
384 /// the caller supplies it.
385 pub(crate) manual_impl_selectors: Vec<ConversionSelector>,
386 /// When set, emit `pub use <path> as sbe_rt;` instead of inlining runtime.
387 pub(crate) external_sbe_rt_path: Option<String>,
388 /// Emit `From<EncodeError/DecodeError>` for this error type path.
389 pub(crate) error_from_path: Option<String>,
390 /// Map enum/boolean NullVal → `Option<T>`. Matched selectors produce
391 /// `Option<EventCode>` accessors instead of bare enum types.
392 /// Wire is byte-identical: `None` writes the `NullVal` discriminant.
393 pub(crate) null_as_option: Vec<ConversionSelector>,
394 /// When true, every enum in the schema gets `Option<Enum>` accessors
395 /// without needing individual `with_null_as_option` calls.
396 pub(crate) all_enums_as_option: bool,
397 /// Emit `bool` ↔ BooleanType converters automatically for every enum
398 /// detected as boolean (name `BooleanType` or `semanticType="Boolean"`).
399 /// Equivalent to calling `with_domain_type(named_type(name), "bool")` for
400 /// each — saves boilerplate on schemas with many boolean flags.
401 pub(crate) auto_bool_domain: bool,
402 /// Emit `_unchecked` companions for benchmarking.
403 /// Appended when a name is a Rust keyword (default `"_"`).
404 pub(crate) keyword_append_token: String,
405 /// Emit `#[deprecated]` on schema-deprecated items (opt-in).
406 pub(crate) deprecated_attrs: bool,
407 /// Emit `Debug`/`Display` impls (default on; pass `false` to shrink output).
408 pub(crate) enable_display_debug: bool,
409 /// Emit meta-attribute constants (default on; pass `false` to shrink output).
410 pub(crate) enable_meta_attributes: bool,
411 /// Emit `AnyMessage`/`FrameCursor`/`MessageVisitor` dispatch (default on).
412 pub(crate) enable_dispatch: bool,
413 /// Hooks fired after each generated item (enum, set, composite, message).
414 /// Returned tokens are appended after the item's definition.
415 pub(crate) hooks: Hooks,
416}
417
418impl std::fmt::Debug for GenerationConfig {
419 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
420 f.debug_struct("GenerationConfig")
421 .field("module_name", &self.module_name)
422 .field("shared_module", &self.shared_module)
423 .field("domain_objects", &self.domain_objects)
424 .field("domain_var_data", &self.domain_var_data)
425 .field("conversions", &self.conversions)
426 .field("domain_types", &self.domain_types)
427 .field("manual_impl_selectors", &self.manual_impl_selectors)
428 .field("external_sbe_rt_path", &self.external_sbe_rt_path)
429 .field("error_from_path", &self.error_from_path)
430 .field("null_as_option", &self.null_as_option)
431 .field("all_enums_as_option", &self.all_enums_as_option)
432 .field("auto_bool_domain", &self.auto_bool_domain)
433 .field("keyword_append_token", &self.keyword_append_token)
434 .field("deprecated_attrs", &self.deprecated_attrs)
435 .field("enable_display_debug", &self.enable_display_debug)
436 .field("enable_meta_attributes", &self.enable_meta_attributes)
437 .field("enable_dispatch", &self.enable_dispatch)
438 .field("hooks", &self.hooks)
439 .finish()
440 }
441}
442
443impl GenerationConfig {
444 /// Create a config for output module `{module_name}.rs` with
445 /// [`GenerationProfile::Full`] defaults.
446 ///
447 /// ```rust
448 /// use ergo_sbe::GenerationConfig;
449 /// let c = GenerationConfig::new("msgs");
450 /// ```
451 #[must_use]
452 pub fn new(module_name: impl Into<String>) -> Self {
453 Self {
454 module_name: module_name.into(),
455 shared_module: None,
456 domain_objects: false,
457 domain_var_data: DomainVarData::Bytes,
458 conversions: Vec::new(),
459 domain_types: Vec::new(),
460 manual_impl_selectors: Vec::new(),
461 external_sbe_rt_path: None,
462 error_from_path: None,
463 keyword_append_token: "_".into(),
464 deprecated_attrs: false,
465 null_as_option: Vec::new(),
466 all_enums_as_option: false,
467 auto_bool_domain: false,
468 enable_display_debug: true,
469 enable_meta_attributes: true,
470 enable_dispatch: true,
471 hooks: Hooks::default(),
472 }
473 }
474
475 /// Create a config with [`GenerationProfile::Lean`] defaults (no
476 /// Display/Debug, no meta attributes, no dispatch, no domain objects).
477 ///
478 /// Equivalent to `GenerationConfig::new(name).profile(GenerationProfile::Lean)`
479 /// but more direct. Explicit `with_*` settings (conversions, domain types,
480 /// auto-bool) can be added after — they are not cleared.
481 ///
482 /// ```rust
483 /// use ergo_sbe::GenerationConfig;
484 /// let c = GenerationConfig::lean("minimal");
485 /// ```
486 #[must_use]
487 pub fn lean(module_name: impl Into<String>) -> Self {
488 Self::new(module_name).profile(GenerationProfile::Lean)
489 }
490
491 /// The module name for generated output.
492 #[must_use]
493 pub(crate) fn module_name(&self) -> &str {
494 &self.module_name
495 }
496
497 /// Override the module name set in [`new`](Self::new). Use when cloning a base
498 /// config across several schemas — set the placeholder in [`new`](Self::new), then
499 /// call `.clone().with_module_name("orderbook")` on each.
500 ///
501 /// ```rust
502 /// use ergo_sbe::GenerationConfig;
503 /// let base = GenerationConfig::new("_");
504 /// let a = base.clone().with_module_name("md");
505 /// let b = base.clone().with_module_name("ob");
506 /// // `a` generates `md.rs`, `b` generates `ob.rs`, `base` unchanged.
507 /// ```
508 #[must_use]
509 pub fn with_module_name(mut self, name: impl Into<String>) -> Self {
510 let name = name.into();
511 debug_assert!(
512 is_valid_module_ident(&name),
513 "module name '{name}' contains path separators, '.', '..', or is empty"
514 );
515 self.module_name = name;
516 self
517 }
518
519 #[must_use]
520 pub(crate) fn domain_objects_enabled(&self) -> bool {
521 self.domain_objects
522 }
523
524 pub(crate) fn has_conversions(&self) -> bool {
525 // `with_bool_domain_type` is syntax sugar for `with_domain_type` on
526 // each boolean enum — it must also emit TryFromSbe/TryToSbe traits.
527 !self.conversions.is_empty() || !self.domain_types.is_empty() || self.auto_bool_domain
528 }
529
530 /// The external sbe_rt path, if set.
531 #[must_use]
532 pub(crate) fn external_sbe_rt_path(&self) -> Option<&str> {
533 self.external_sbe_rt_path.as_deref()
534 }
535
536 /// Re-use one `sbe_rt` runtime across separately generated schema modules.
537 ///
538 /// `path` must work in `pub use <path> as sbe_rt;`.
539 ///
540 /// ```
541 /// # use ergo_sbe::GenerationConfig;
542 /// // first module embeds sbe_rt; later modules do:
543 /// // pub use crate::common::sbe_rt as sbe_rt;
544 /// GenerationConfig::new("md")
545 /// .with_external_sbe_rt("crate::common::sbe_rt");
546 /// ```
547 #[must_use]
548 pub fn with_external_sbe_rt(mut self, path: impl Into<String>) -> Self {
549 self.external_sbe_rt_path = Some(path.into());
550 self
551 }
552
553 /// Enable **generic** conversion methods for matching fields.
554 ///
555 /// # Generated API
556 ///
557 /// In build.rs: `.with_conversion(ConversionSelector::named_type("Decimal"))`.
558 /// Application code: `enc.price_from(&my_price)?;` / `dec.price_as::<MyPrice>()?`.
559 ///
560 /// # Example
561 ///
562 /// ```rust
563 /// use ergo_sbe::{GenerationConfig, ConversionSelector};
564 ///
565 /// let config = GenerationConfig::new("msgs")
566 /// .with_conversion(ConversionSelector::named_type("Decimal"));
567 /// ```
568 ///
569 /// → [`sbe/tests/comprehensive_test.rs`](https://github.com/mimran1980/ergon/blob/main/sbe/tests/comprehensive_test.rs)
570 ///
571 /// Prefer [`Self::with_domain_type`] when one concrete Rust type is enough.
572 /// Duplicate selectors are ignored; selectors matching nothing error at
573 /// [`crate::Generator::generate`] time.
574 #[must_use]
575 pub fn with_conversion(mut self, selector: ConversionSelector) -> Self {
576 if !self.conversions.contains(&selector) {
577 self.conversions.push(selector);
578 }
579 self
580 }
581
582 /// Map matching enum fields from `NullVal` → `Option<T>`.
583 ///
584 /// Wire encoding is byte-identical: `None` writes the `NullVal`
585 /// discriminant, `Some(v)` writes `v`. Zero runtime cost.
586 ///
587 /// ```rust
588 /// use ergo_sbe::{ConversionSelector, GenerationConfig};
589 ///
590 /// // EventCode fields → Option<EventCode>
591 /// let config = GenerationConfig::new("msgs")
592 /// .with_null_as_option(ConversionSelector::named_type("EventCode"));
593 /// ```
594 #[must_use]
595 pub fn with_null_as_option(mut self, selector: ConversionSelector) -> Self {
596 if !self.null_as_option.contains(&selector) {
597 self.null_as_option.push(selector);
598 }
599 self
600 }
601
602 /// Map **every** enum field in the schema to `Option<Enum>`.
603 ///
604 /// Shorthand for calling [`with_null_as_option`](Self::with_null_as_option)
605 /// on every named type. Opt-out per-enum is not yet supported —
606 /// the blanket flag always wins.
607 ///
608 /// ```rust
609 /// use ergo_sbe::GenerationConfig;
610 /// let config = GenerationConfig::new("msgs")
611 /// .with_all_enums_as_option();
612 /// ```
613 #[must_use]
614 pub fn with_all_enums_as_option(mut self) -> Self {
615 self.all_enums_as_option = true;
616 self
617 }
618
619 /// Map matching fields to a **concrete** Rust type path.
620 ///
621 /// Implies [`Self::with_conversion`] for the same selector and emits
622 /// well-known `TryFromSbe`/`TryToSbe` impls for `bool`,
623 /// `rust_decimal::Decimal`, and `chrono::DateTime<Utc>` when those paths
624 /// are used. For any other `rust_type`, no impl is auto-generated.
625 ///
626 /// To skip the built-in impl and supply your own, use
627 /// [`Self::with_manual_domain_type`].
628 ///
629 /// # Generated API
630 ///
631 /// In build.rs: `.with_domain_type(ConversionSelector::named_type("Decimal"), "rust_decimal::Decimal")`
632 /// Application: `enc.try_price(rust_decimal::Decimal::new(12345, 2))?` / `let p = dec.try_price()?`.
633 ///
634 /// # Example
635 ///
636 /// ```rust
637 /// use ergo_sbe::{GenerationConfig, ConversionSelector};
638 ///
639 /// let config = GenerationConfig::new("msgs")
640 /// .with_domain_type(
641 /// ConversionSelector::named_type("Decimal"),
642 /// "rust_decimal::Decimal",
643 /// );
644 /// ```
645 ///
646 /// Do **not** also call [`Self::with_conversion`] for the same selector.
647 #[must_use]
648 pub fn with_domain_type(
649 self,
650 selector: ConversionSelector,
651 rust_type: impl Into<String>,
652 ) -> Self {
653 self.apply_domain_type(selector, rust_type, DomainImpl::Generated)
654 }
655
656 /// Like [`Self::with_domain_type`], but you write `impl TryFromSbe<Wire>`
657 /// / `impl TryToSbe<Wire>` yourself — e.g. a custom rounding rule, or
658 /// null/validation behaviour the built-in impl does not match.
659 ///
660 /// ergo-sbe still generates the concrete `try_price(...)?` / `try_price()?`
661 /// signatures that call those impls. A missing impl fails closed with a
662 /// named compile error, and for the three built-ins the generated
663 /// accessor's rustdoc carries the exact impl `Generated` would have
664 /// written, ready to copy and adjust.
665 ///
666 /// ```rust
667 /// use ergo_sbe::{GenerationConfig, ConversionSelector};
668 ///
669 /// let config = GenerationConfig::new("msgs")
670 /// .with_manual_domain_type(
671 /// ConversionSelector::named_type("Decimal"),
672 /// "rust_decimal::Decimal",
673 /// );
674 /// // Application code must provide:
675 /// // impl TryFromSbe<Decimal> for rust_decimal::Decimal { ... }
676 /// // impl TryToSbe<Decimal> for rust_decimal::Decimal { ... }
677 /// ```
678 #[must_use]
679 pub fn with_manual_domain_type(
680 self,
681 selector: ConversionSelector,
682 rust_type: impl Into<String>,
683 ) -> Self {
684 self.apply_domain_type(selector, rust_type, DomainImpl::Manual)
685 }
686
687 fn apply_domain_type(
688 mut self,
689 selector: ConversionSelector,
690 rust_type: impl Into<String>,
691 impl_kind: DomainImpl,
692 ) -> Self {
693 let sel = selector;
694 let ty = rust_type.into();
695 if !self.conversions.contains(&sel) {
696 self.conversions.push(sel.clone());
697 }
698 match impl_kind {
699 DomainImpl::Generated => self.manual_impl_selectors.retain(|s| s != &sel),
700 DomainImpl::Manual => {
701 if !self.manual_impl_selectors.contains(&sel) {
702 self.manual_impl_selectors.push(sel.clone());
703 }
704 }
705 }
706 // Last-write-wins: calling with_domain_type(sel, "B") after
707 // with_domain_type(sel, "A") replaces the mapping.
708 if let Some(existing) = self.domain_types.iter_mut().find(|(s, _)| s == &sel) {
709 existing.1 = ty;
710 } else {
711 self.domain_types.push((sel, ty));
712 }
713 self
714 }
715
716 /// Emit `From<sbe_rt::EncodeError>` / `From<sbe_rt::DecodeError>` for your error type.
717 ///
718 /// In build.rs: `.with_error_from_impls("crate::AppError")`.
719 /// Application code: `enc.group(...)?;` — `EncodeError` auto-converts via `From`.
720 ///
721 /// **Note:** The generated `From` impl uses `format!("sbe encode: {err}")` —
722 /// stringifying the typed error through its `Display` form, then calling
723 /// `YourType::from(String)`. This means (1) your error type must implement
724 /// `From<String>`, and (2) field-level error details (e.g.
725 /// `EncodeError::BufferTooShort { field, needed, available }`) are lost in
726 /// the conversion. Implement `From<generated::sbe_rt::EncodeError>` and
727 /// `From<generated::sbe_rt::DecodeError>` on your error type so those
728 /// fields survive. Removal is scheduled for 1.0.
729 #[must_use]
730 #[deprecated(
731 since = "0.1.20",
732 note = "implement From<generated::sbe_rt::EncodeError> and From<generated::sbe_rt::DecodeError> on your error type so wire fields (needed/available) are preserved; this helper formats through String and will be removed in 1.0"
733 )]
734 pub fn with_error_from_impls(mut self, path: impl Into<String>) -> Self {
735 self.error_from_path = Some(path.into());
736 self
737 }
738
739 /// Generate owned domain structs next to flyweight codecs.
740 ///
741 /// # `var_data` — important choice ([`DomainVarData`])
742 ///
743 /// | Mode | DTO field | Invalid UTF-8 |
744 /// |------|-----------|---------------|
745 /// | [`DomainVarData::Bytes`] | `Vec<u8>` | n/a |
746 /// | [`DomainVarData::Strings`] | `String` | `InvalidUtf8` error (strict) |
747 ///
748 /// ```rust
749 /// use ergo_sbe::{DomainVarData, GenerationConfig};
750 /// let text = GenerationConfig::new("msgs")
751 /// .with_domain_objects(DomainVarData::Strings);
752 /// let bytes = GenerationConfig::new("msgs")
753 /// .with_domain_objects(DomainVarData::Bytes);
754 /// let _ = (text, bytes);
755 /// ```
756 ///
757 /// # Generated API
758 ///
759 /// `DomainVarData::Strings` → `manufacturer: String`.
760 /// `DomainVarData::Bytes` → `manufacturer: Vec<u8>`.
761 ///
762 /// → [`sbe/tests/domain_objects_test.rs`](https://github.com/mimran1980/ergon/blob/main/sbe/tests/domain_objects_test.rs)
763 #[must_use]
764 pub fn with_domain_objects(mut self, var_data: DomainVarData) -> Self {
765 self.domain_objects = true;
766 self.domain_var_data = var_data;
767 self
768 }
769
770 /// Shared module name for multi-schema generation ([`crate::Generator::generate_multi`]).
771 ///
772 /// First schema owns shared enums/sets/composites; later modules
773 /// `pub use super::<name>::*`.
774 #[must_use]
775 pub fn with_shared_module(mut self, name: impl Into<String>) -> Self {
776 self.shared_module = Some(name.into());
777 self
778 }
779
780 /// Token appended when a schema name is a Rust keyword (default `”_”`).
781 ///
782 /// Schema field `name="type"` becomes method `type_()`; with token `"x"`,
783 /// it becomes `typex()`.
784 ///
785 /// ```rust
786 /// use ergo_sbe::GenerationConfig;
787 /// let c = GenerationConfig::new("m").with_keyword_append_token("_");
788 /// let _ = c;
789 /// ```
790 #[must_use]
791 pub fn with_keyword_append_token(mut self, token: impl Into<String>) -> Self {
792 self.keyword_append_token = token.into();
793 self
794 }
795
796 /// Auto-register `bool` converters for every boolean enum in the
797 /// schema. Syntax sugar for calling
798 /// `with_domain_type(named_type("BooleanType"), "bool")` for each —
799 /// detects by name, `semanticType="Boolean"`, or True/False value pairs
800 /// with discriminants `0` and `1`.
801 ///
802 /// Only the canonical `{0, 1}` discriminant representation is detected
803 /// automatically. Schemas with non-standard boolean encodings (e.g.
804 /// `Yes=5, No=3`) should use explicit [`ConversionSelector::named_type`]
805 /// with [`GenerationConfig::with_conversion`] instead.
806 #[must_use]
807 pub fn with_bool_domain_type(mut self, enable: bool) -> Self {
808 self.auto_bool_domain = enable;
809 self
810 }
811
812 /// Emit `#[deprecated]` on schema-deprecated fields/types/messages.
813 #[must_use]
814 pub fn with_deprecated_attrs(mut self, enable: bool) -> Self {
815 self.deprecated_attrs = enable;
816 self
817 }
818
819 /// Control generated `Debug` and `Display` impls (**enabled by default**).
820 /// Pass `false` to omit them and shrink generated output.
821 #[must_use]
822 pub fn with_display_debug(mut self, enable: bool) -> Self {
823 self.enable_display_debug = enable;
824 self
825 }
826
827 /// Control meta-attribute constants (**enabled by default**). Pass `false`
828 /// to omit — removes `*_meta_attribute`, `*_ENCODING_OFFSET`,
829 /// `*_ENCODING_LENGTH`, `*_ID`, `*_SINCE_VERSION`, null/min/max field
830 /// constants, and the per-message `*_field_meta` module.
831 #[must_use]
832 pub fn with_meta_attributes(mut self, enable: bool) -> Self {
833 self.enable_meta_attributes = enable;
834 self
835 }
836
837 /// Control `AnyMessage` / `FrameCursor` / `MessageVisitor` dispatch code
838 /// (**enabled by default**). Pass `false` to omit — saves ~300 lines;
839 /// only meaningful when you do not need multi-template frame dispatch.
840 #[must_use]
841 pub fn with_dispatch(mut self, enable: bool) -> Self {
842 self.enable_dispatch = enable;
843 self
844 }
845
846 /// Apply a product profile that sets the size knobs together.
847 ///
848 /// | Profile | Display/Debug | Meta attrs | Dispatch | Domain objects |
849 /// |---------|---------------|------------|----------|----------------|
850 /// | [`GenerationProfile::Full`] | on | on | on | unchanged |
851 /// | [`GenerationProfile::Lean`] | off | off | off | off |
852 ///
853 /// Explicit `with_*` settings (conversions, domain types, auto-bool) win
854 /// regardless of order — `profile()` only sets the knobs it owns and
855 /// never clears explicit configuration. Prefer
856 /// [`lean`](Self::lean) for a clean Lean baseline.
857 ///
858 /// Chain further `with_*` calls after `profile` to override individual
859 /// knobs. Example:
860 ///
861 /// ```rust
862 /// use ergo_sbe::{GenerationConfig, GenerationProfile};
863 /// let _ = GenerationConfig::new("feed").profile(GenerationProfile::Lean);
864 /// ```
865 #[must_use]
866 pub fn profile(mut self, profile: GenerationProfile) -> Self {
867 match profile {
868 GenerationProfile::Full => {
869 self.enable_display_debug = true;
870 self.enable_meta_attributes = true;
871 self.enable_dispatch = true;
872 }
873 GenerationProfile::Lean => {
874 self.enable_display_debug = false;
875 self.enable_meta_attributes = false;
876 self.enable_dispatch = false;
877 self.domain_objects = false;
878 // ponytail: explicit conversion/domain-type settings survive
879 // profile() — they represent deliberate schema choices with
880 // higher precedence than a bulk surface preset.
881 }
882 }
883 self
884 }
885
886 /// Register a code-generation hook. The closure receives an
887 /// [`ItemContext`] for each generated item (enum, set, composite,
888 /// message decoder/encoder, domain struct) and returns token streams
889 /// appended after the item's definition.
890 ///
891 /// Hooks fire in registration order. Use [`quote::quote!`] in your
892 /// closure body to build the returned tokens.
893 ///
894 /// # Example — serde `Serialize` for enums
895 ///
896 /// ```rust
897 /// use ergo_sbe::{GenerationConfig, ItemContext};
898 /// use quote::quote;
899 ///
900 /// let config = GenerationConfig::new("msgs")
901 /// .with_hook(|ctx: &ItemContext| -> Vec<proc_macro2::TokenStream> {
902 /// match ctx {
903 /// ItemContext::Enum { name, variants, .. } => {
904 /// // Manual Serialize impl appends after the enum definition
905 /// vec![quote! { /* impl Serialize for ... */ }]
906 /// }
907 /// _ => vec![],
908 /// }
909 /// });
910 /// ```
911 #[must_use]
912 pub fn with_hook<F>(mut self, hook: F) -> Self
913 where
914 F: Fn(&ItemContext) -> Vec<proc_macro2::TokenStream> + Send + Sync + 'static,
915 {
916 self.hooks.push(std::sync::Arc::new(hook));
917 self
918 }
919
920 /// True when at least one hook is registered.
921 pub(crate) fn has_hooks(&self) -> bool {
922 !self.hooks.is_empty()
923 }
924
925 /// Iterate all registered hooks.
926 pub(crate) fn run_hooks(&self, ctx: &ItemContext, out: &mut String) {
927 for hook in self.hooks.iter() {
928 for ts in hook(ctx) {
929 // Use TokenStream Display impl for formatting.
930 // For simple impl blocks this produces valid Rust.
931 use std::fmt::Write;
932 let _ = writeln!(out, "{}", ts);
933 }
934 }
935 }
936}
937
938impl Default for GenerationConfig {
939 fn default() -> Self {
940 Self::new("messages")
941 }
942}
943
944/// Reject module names that are not valid for `mod name;` in generated Rust.
945///
946/// `syn::parse_str::<syn::Ident>` alone accepts keywords (they parse as idents
947/// in isolation). We additionally reject strict keywords and reserved words
948/// that require a raw identifier (`r#gen`) under current/edition-reserved
949/// Rust editions — including 2024's `gen`.
950pub(crate) fn is_valid_module_ident(name: &str) -> bool {
951 if name.is_empty()
952 || name.contains('/')
953 || name.contains('\\')
954 || name.contains('.')
955 || name == ".."
956 {
957 return false;
958 }
959 if is_rust_keyword_or_reserved(name) {
960 return false;
961 }
962 syn::parse_str::<syn::Ident>(name).is_ok()
963}
964
965/// Strict keywords + reserved identifiers that cannot appear as a plain
966/// `mod name;` without `r#` escaping.
967fn is_rust_keyword_or_reserved(name: &str) -> bool {
968 // Keep in sync with the Rust reference keyword tables (strict + reserved
969 // + edition-reserved). Weak keywords (`union`, `macro_rules`, …) remain
970 // allowed as module names.
971 matches!(
972 name,
973 // Strict
974 "as" | "break" | "const" | "continue" | "crate" | "else" | "enum" | "extern"
975 | "false" | "fn" | "for" | "if" | "impl" | "in" | "let" | "loop" | "match"
976 | "mod" | "move" | "mut" | "pub" | "ref" | "return" | "self" | "Self"
977 | "static" | "struct" | "super" | "trait" | "true" | "type" | "unsafe"
978 | "use" | "where" | "while"
979 // 2018+
980 | "async" | "await" | "dyn"
981 // Reserved
982 | "abstract" | "become" | "box" | "do" | "final" | "macro" | "override"
983 | "priv" | "typeof" | "unsized" | "virtual" | "yield"
984 // Reserved / edition-reserved
985 | "try" | "gen"
986 )
987}
988
989#[cfg(test)]
990mod tests {
991 use super::{ConversionSelector, DomainVarData, GenerationConfig, GenerationProfile};
992
993 #[test]
994 fn default_config_is_clean() -> Result<(), Box<dyn std::error::Error>> {
995 let config = GenerationConfig::default();
996 assert_eq!(config.module_name(), "messages");
997 assert!(!config.domain_objects_enabled());
998 assert!(!config.has_conversions());
999 Ok(())
1000 }
1001
1002 #[test]
1003 fn with_conversion_adds_selector() -> Result<(), Box<dyn std::error::Error>> {
1004 let config = GenerationConfig::new("test")
1005 .with_conversion(ConversionSelector::named_type("Decimal"));
1006 assert!(config.has_conversions());
1007 assert_eq!(config.conversions.len(), 1);
1008 Ok(())
1009 }
1010
1011 #[test]
1012 fn profile_lean_preserves_explicit_conversions_and_domain_types()
1013 -> Result<(), Box<dyn std::error::Error>> {
1014 let full = GenerationConfig::new("m").profile(GenerationProfile::Full);
1015 assert!(full.enable_display_debug);
1016 assert!(full.enable_meta_attributes);
1017 assert!(full.enable_dispatch);
1018
1019 let lean = GenerationConfig::new("m")
1020 .with_domain_objects(DomainVarData::Bytes)
1021 .with_conversion(ConversionSelector::named_type("Decimal"))
1022 .with_domain_type(
1023 ConversionSelector::named_type("Decimal"),
1024 "rust_decimal::Decimal",
1025 )
1026 .profile(GenerationProfile::Lean);
1027 assert!(!lean.enable_display_debug);
1028 assert!(!lean.enable_meta_attributes);
1029 assert!(!lean.enable_dispatch);
1030 assert!(!lean.domain_objects);
1031 // Explicit conversions and domain types survive Lean — they're
1032 // deliberate schema choices with higher precedence than a bulk preset.
1033 assert!(
1034 lean.has_conversions(),
1035 "explicit conversions must survive Lean"
1036 );
1037 assert!(
1038 !lean.domain_types.is_empty(),
1039 "explicit domain types must survive Lean"
1040 );
1041
1042 // Later overrides still win.
1043 let override_dispatch = GenerationConfig::new("m")
1044 .profile(GenerationProfile::Lean)
1045 .with_dispatch(true);
1046 assert!(override_dispatch.enable_dispatch);
1047 Ok(())
1048 }
1049
1050 #[test]
1051 fn with_conversion_dedup() -> Result<(), Box<dyn std::error::Error>> {
1052 let config = GenerationConfig::new("test")
1053 .with_conversion(ConversionSelector::named_type("Decimal"))
1054 .with_conversion(ConversionSelector::named_type("Decimal"));
1055 assert_eq!(config.conversions.len(), 1);
1056 Ok(())
1057 }
1058
1059 #[test]
1060 fn with_domain_type_adds_conversion_and_type() -> Result<(), Box<dyn std::error::Error>> {
1061 let config = GenerationConfig::new("test").with_domain_type(
1062 ConversionSelector::named_type("Decimal"),
1063 "rust_decimal::Decimal",
1064 );
1065 assert!(config.has_conversions());
1066 assert_eq!(config.conversions.len(), 1);
1067 assert_eq!(config.domain_types.len(), 1);
1068 assert!(config.manual_impl_selectors.is_empty());
1069 Ok(())
1070 }
1071
1072 #[test]
1073 fn with_domain_type_two_argument_calls_agree() -> Result<(), Box<dyn std::error::Error>> {
1074 let sel = ConversionSelector::named_type("Decimal");
1075 let a =
1076 GenerationConfig::new("msgs").with_domain_type(sel.clone(), "rust_decimal::Decimal");
1077 let b = GenerationConfig::new("msgs").with_domain_type(sel, "rust_decimal::Decimal");
1078 assert_eq!(a.domain_types, b.domain_types);
1079 assert_eq!(a.manual_impl_selectors, b.manual_impl_selectors);
1080 Ok(())
1081 }
1082
1083 #[test]
1084 fn with_manual_domain_type_is_additive() -> Result<(), Box<dyn std::error::Error>> {
1085 let sel = ConversionSelector::named_type("Decimal");
1086 let config = GenerationConfig::new("msgs")
1087 .with_manual_domain_type(sel.clone(), "rust_decimal::Decimal");
1088 assert_eq!(config.domain_types.len(), 1);
1089 assert_eq!(config.manual_impl_selectors, vec![sel]);
1090 Ok(())
1091 }
1092
1093 #[test]
1094 fn with_domain_type_dedup() -> Result<(), Box<dyn std::error::Error>> {
1095 let config = GenerationConfig::new("test")
1096 .with_domain_type(
1097 ConversionSelector::named_type("Decimal"),
1098 "rust_decimal::Decimal",
1099 )
1100 .with_domain_type(
1101 ConversionSelector::named_type("Decimal"),
1102 "rust_decimal::Decimal",
1103 );
1104 assert_eq!(config.domain_types.len(), 1);
1105 Ok(())
1106 }
1107
1108 #[test]
1109 fn with_external_sbe_rt_sets_path() -> Result<(), Box<dyn std::error::Error>> {
1110 let config = GenerationConfig::new("m").with_external_sbe_rt("crate::rt::sbe_rt");
1111 assert_eq!(config.external_sbe_rt_path(), Some("crate::rt::sbe_rt"));
1112 Ok(())
1113 }
1114
1115 #[test]
1116 fn module_ident_rejects_keywords_and_reserved() -> Result<(), Box<dyn std::error::Error>> {
1117 use super::is_valid_module_ident;
1118 assert!(is_valid_module_ident("messages"));
1119 assert!(is_valid_module_ident("common_types"));
1120 assert!(!is_valid_module_ident("gen")); // Rust 2024 reserved
1121 assert!(!is_valid_module_ident("mod"));
1122 assert!(!is_valid_module_ident("async"));
1123 assert!(!is_valid_module_ident("try"));
1124 assert!(!is_valid_module_ident(""));
1125 assert!(!is_valid_module_ident("a.b"));
1126 Ok(())
1127 }
1128
1129 #[test]
1130 fn new_config_has_correct_defaults() -> Result<(), Box<dyn std::error::Error>> {
1131 let config = GenerationConfig::new("mymod");
1132 assert_eq!(config.module_name(), "mymod");
1133 assert!(!config.domain_objects_enabled());
1134 assert!(config.conversions.is_empty());
1135 assert!(config.domain_types.is_empty());
1136 assert_eq!(config.domain_var_data, DomainVarData::Bytes);
1137 assert!(config.enable_display_debug);
1138 assert!(config.enable_meta_attributes);
1139 assert!(config.enable_dispatch);
1140 Ok(())
1141 }
1142
1143 #[test]
1144 fn with_domain_objects_var_data_modes() -> Result<(), Box<dyn std::error::Error>> {
1145 let text = GenerationConfig::new("m").with_domain_objects(DomainVarData::Strings);
1146 assert!(text.domain_objects_enabled());
1147 assert_eq!(text.domain_var_data, DomainVarData::Strings);
1148 let bytes = GenerationConfig::new("m").with_domain_objects(DomainVarData::Bytes);
1149 assert!(bytes.domain_objects_enabled());
1150 assert_eq!(bytes.domain_var_data, DomainVarData::Bytes);
1151 Ok(())
1152 }
1153
1154 #[test]
1155 fn opt_in_codegen_flags_and_field_selector_are_recorded()
1156 -> Result<(), Box<dyn std::error::Error>> {
1157 let selector = ConversionSelector::field_path("Order.price");
1158 assert_eq!(
1159 selector,
1160 ConversionSelector::FieldPath("Order.price".to_string())
1161 );
1162
1163 #[allow(deprecated)]
1164 let config = GenerationConfig::new("m")
1165 .with_error_from_impls("crate::AppError")
1166 .with_shared_module("shared")
1167 .with_keyword_append_token("x")
1168 .with_bool_domain_type(true)
1169 .with_deprecated_attrs(true);
1170
1171 assert_eq!(config.error_from_path.as_deref(), Some("crate::AppError"));
1172 assert_eq!(config.shared_module.as_deref(), Some("shared"));
1173 assert_eq!(config.keyword_append_token, "x");
1174 assert!(config.auto_bool_domain);
1175 assert!(config.deprecated_attrs);
1176 Ok(())
1177 }
1178}