1use std::collections::HashSet;
47use std::fmt::Write;
48
49use crate::ir::{ByteOrder, Ir, Presence, PrimitiveType, Signal, Token};
50use crate::structured_ir::*;
51use crate::{GenerationConfig, Schema};
52
53pub(crate) mod conversion_helpers;
54pub(crate) use conversion_helpers::*;
55pub(crate) mod conversion_traits;
56pub(crate) use conversion_traits::*;
57pub(crate) mod converter_impls;
58pub(crate) use converter_impls::generate_converter_impls;
59pub(crate) mod decoder_display;
60pub(crate) use decoder_display::generate_decoder_display;
61pub(crate) mod domain_cluster;
62pub(crate) use domain_cluster::*;
63pub(crate) mod encoded_length;
64pub(crate) mod field_type;
65pub(crate) use field_type::field_type_ident;
66pub(crate) mod message_header_template;
67pub(crate) use message_header_template::*;
68pub(crate) mod nullification;
69pub(crate) use nullification::*;
70pub(crate) mod runtime;
71use quote::format_ident;
72pub(crate) use runtime::*;
73pub(crate) mod group_encoder;
74pub(crate) use group_encoder::generate_group_encoder;
75pub(crate) mod group_decoder;
76pub(crate) use group_decoder::generate_group_decoder;
77pub(crate) mod tail_stages;
78pub(crate) use tail_stages::*;
79pub(crate) mod message_decoder;
80pub(crate) use message_decoder::generate_message_decoder;
81pub(crate) mod message_encoder;
82pub(crate) use message_encoder::generate_message_encoder;
83use sha2::{Digest, Sha256};
84
85#[derive(Clone, Debug, Eq, PartialEq)]
90pub struct GeneratedModule {
91 pub path: String,
93 pub source: String,
95}
96
97#[derive(Clone, Debug, Default, Eq, PartialEq)]
113pub struct GeneratedModuleSet {
114 modules: Vec<GeneratedModule>,
115 warnings: Vec<String>,
117}
118
119#[derive(Clone, Debug, Eq, PartialEq)]
122#[non_exhaustive]
123pub enum GenerateError {
124 HeaderValueOutOfRange {
127 field: String,
129 value: u64,
131 maximum: u64,
133 context: String,
135 },
136 InvalidConversion {
138 selector: String,
140 reason: String,
142 },
143 ConversionCollision {
145 method: String,
147 selector_a: String,
149 selector_b: String,
151 },
152 InvalidGeneratedSource {
155 module: String,
157 error: String,
159 },
160 InvalidConfiguration {
162 option: String,
164 value: String,
166 reason: String,
168 },
169 IncompatibleSharedType {
172 name: String,
174 owner_module: String,
176 consumer_module: String,
178 difference: String,
180 },
181}
182
183impl core::fmt::Display for GenerateError {
184 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
185 match self {
186 Self::HeaderValueOutOfRange {
187 field,
188 value,
189 maximum,
190 context,
191 } => {
192 write!(
193 f,
194 "message header field '{field}' value {value} for {context} exceeds declared maximum {maximum}"
195 )
196 }
197 Self::InvalidConversion { selector, reason } => {
198 write!(f, "invalid conversion '{selector}': {reason}")
199 }
200 Self::ConversionCollision {
201 method,
202 selector_a,
203 selector_b,
204 } => {
205 write!(
206 f,
207 "conversion method collision: '{method}' from '{selector_a}' and '{selector_b}'"
208 )
209 }
210 Self::InvalidGeneratedSource { module, error } => {
211 write!(
212 f,
213 "generated module '{module}' failed Rust syntax validation: {error}"
214 )
215 }
216 Self::InvalidConfiguration {
217 option,
218 value,
219 reason,
220 } => {
221 write!(
222 f,
223 "invalid configuration option '{option}': value '{value}' — {reason}"
224 )
225 }
226 Self::IncompatibleSharedType {
227 name,
228 owner_module,
229 consumer_module,
230 difference,
231 } => {
232 write!(
233 f,
234 "shared type '{name}' is wire-incompatible between modules \
235 '{owner_module}' (owner) and '{consumer_module}': {difference}"
236 )
237 }
238 }
239 }
240}
241
242impl core::error::Error for GenerateError {}
243
244impl GeneratedModuleSet {
245 pub(crate) fn push(&mut self, module: GeneratedModule) {
246 self.modules.push(module);
247 }
248
249 #[must_use]
251 pub fn modules(&self) -> impl ExactSizeIterator<Item = &GeneratedModule> {
252 self.modules.iter()
253 }
254
255 #[must_use]
258 pub fn warnings(&self) -> &[String] {
259 &self.warnings
260 }
261
262 #[must_use]
281 pub fn into_parts(self) -> (Vec<GeneratedModule>, Vec<String>) {
282 (self.modules, self.warnings)
283 }
284}
285
286#[allow(missing_docs)]
290pub(crate) struct GenerationContext {
291 pub elements: SchemaElements,
292 pub byte_order: ByteOrder,
293 pub schema_id: u16,
294 pub schema_version: u16,
295 pub header_type: String,
296 pub header_size: usize,
297 pub schema_name: String,
298 pub multi_message: bool,
299 pub conversions: Vec<crate::ConversionSelector>,
300 pub domain_types: Vec<(crate::ConversionSelector, String)>,
301 pub domain_objects: bool,
302 pub domain_var_data: crate::config::DomainVarData,
303 pub enable_display_debug: bool,
304 pub enable_meta_attributes: bool,
305 pub enable_dispatch: bool,
306}
307
308#[derive(Debug)]
330pub struct Generator {
331 config: GenerationConfig,
332}
333
334impl Generator {
335 #[must_use]
337 pub const fn new(config: GenerationConfig) -> Self {
338 Self { config }
339 }
340
341 fn validate_header_values(&self, schema: &Schema) -> Result<(), GenerateError> {
342 let elements = partition_tokens(&schema.ir.tokens);
343 let Some(header) = elements
344 .composites
345 .iter()
346 .find(|tokens| tokens[0].name == schema.ir.header_type)
347 else {
348 return Ok(());
352 };
353
354 let check = |field_name: &str, value: u64, context: String| -> Result<(), GenerateError> {
355 let Some(field) = header
356 .iter()
357 .find(|token| token.signal == Signal::BeginField && token.name == field_name)
358 else {
359 return Ok(());
360 };
361 if field.encoding.presence == Presence::Constant {
362 return Ok(());
363 }
364 let maximum = field.encoding.max_value.unwrap_or(u64::MAX);
365 if value > maximum {
366 return Err(GenerateError::HeaderValueOutOfRange {
367 field: field_name.to_string(),
368 value,
369 maximum,
370 context,
371 });
372 }
373 Ok(())
374 };
375
376 let schema_context = format!("schema '{}'", schema.package);
377 check("schemaId", u64::from(schema.id), schema_context.clone())?;
378 check("version", u64::from(schema.version), schema_context)?;
379
380 for message_tokens in &elements.messages {
381 let message = parse_message_structure(message_tokens, &elements);
382 let context = format!("message '{}'", message.name);
383 check("templateId", u64::from(message.id), context.clone())?;
384 let block_length = u64::try_from(message.block_length).unwrap_or(u64::MAX);
385 check("blockLength", block_length, context)?;
386 }
387
388 Ok(())
389 }
390
391 fn validate_conversions(&self, schema: &Schema) -> Result<(), GenerateError> {
392 if !self.config.has_conversions() {
393 return Ok(());
394 }
395 let elements = partition_tokens(&schema.ir.tokens);
396 for sel in &self.config.conversions {
397 let matched = match sel {
398 crate::ConversionSelector::NamedType(name) => {
399 elements.composites.iter().any(|c| c[0].name == *name)
400 || elements.enums.iter().any(|e| e[0].name == *name)
401 || elements.sets.iter().any(|s| s[0].name == *name)
402 }
403 crate::ConversionSelector::SemanticType(_) => {
404 true
407 }
408 crate::ConversionSelector::FieldPath(_) => {
409 true
411 }
412 };
413 if !matched {
414 return Err(GenerateError::InvalidConversion {
415 selector: format!("{sel:?}"),
416 reason: "no matching type found in schema".into(),
417 });
418 }
419 }
420 for (sel, rust_type) in &self.config.domain_types {
421 if rust_type.is_empty() {
422 return Err(GenerateError::InvalidConversion {
423 selector: format!("{sel:?}"),
424 reason: "domain type path must not be empty".into(),
425 });
426 }
427 syn::parse_str::<syn::Type>(rust_type).map_err(|e| {
430 GenerateError::InvalidConversion {
431 selector: format!("{sel:?}"),
432 reason: format!("domain type path is not a valid Rust type: {e}"),
433 }
434 })?;
435 }
436 Ok(())
437 }
438
439 fn validate_conversions_union(
444 &self,
445 union: &[(&Schema, crate::structured_ir::SchemaElements)],
446 ) -> Result<(), GenerateError> {
447 if !self.config.has_conversions() {
448 return Ok(());
449 }
450 for sel in &self.config.conversions {
451 if let crate::ConversionSelector::NamedType(name) = sel {
452 let matched = union.iter().any(|(_, elements)| {
453 elements.composites.iter().any(|c| c[0].name == *name)
454 || elements.enums.iter().any(|e| e[0].name == *name)
455 || elements.sets.iter().any(|s| s[0].name == *name)
456 });
457 if !matched {
458 return Err(GenerateError::InvalidConversion {
459 selector: format!("{sel:?}"),
460 reason: "no matching type found in any schema".into(),
461 });
462 }
463 }
464 }
466 for (sel, rust_type) in &self.config.domain_types {
467 syn::parse_str::<syn::Type>(rust_type).map_err(|e| {
468 GenerateError::InvalidConversion {
469 selector: format!("{sel:?}"),
470 reason: format!("domain type path is not a valid Rust type: {e}"),
471 }
472 })?;
473 }
474 Ok(())
475 }
476
477 fn validate_paths(&self) -> Result<(), GenerateError> {
480 let mn = self.config.module_name();
482 if !crate::config::is_valid_module_ident(mn) {
483 return Err(GenerateError::InvalidConfiguration {
484 option: "module_name".into(),
485 value: mn.into(),
486 reason:
487 "module name must be a single Rust identifier — no '/', '\\\\', '.', or '..'"
488 .into(),
489 });
490 }
491 if let Some(ref err_path) = self.config.error_from_path {
492 syn::parse_str::<syn::Type>(err_path).map_err(|e| {
493 GenerateError::InvalidConversion {
494 selector: "error_from_path".into(),
495 reason: format!("error-from path is not a valid Rust type: {e}"),
496 }
497 })?;
498 }
499 if let Some(ref rt_path) = self.config.external_sbe_rt_path {
500 syn::parse_str::<syn::Path>(rt_path).map_err(|e| {
501 GenerateError::InvalidConfiguration {
502 option: "external_sbe_rt".into(),
503 value: rt_path.clone(),
504 reason: format!("not a valid Rust path: {e}"),
505 }
506 })?;
507 }
508 Ok(())
509 }
510
511 #[allow(missing_docs)]
512 fn effective_domain_types(
513 &self,
514 schemas: &[(&Schema, &str)],
515 ) -> Vec<(crate::ConversionSelector, String)> {
516 let mut types = self.config.domain_types.clone();
517 if self.config.auto_bool_domain {
518 for (schema, _) in schemas {
519 let elements = partition_tokens(&schema.ir.tokens);
520 for e in &elements.enums {
521 let name = &e[0].name;
522 if crate::structured_ir::is_bool_value_enum(&elements, name) {
523 let sel = crate::ConversionSelector::named_type(name);
524 if !types.iter().any(|(s, _)| s == &sel) {
525 types.push((sel, "bool".into()));
526 }
527 }
528 }
529 }
530 }
531 types
532 }
533
534 pub fn generate(&self, schema: &Schema) -> Result<GeneratedModuleSet, GenerateError> {
540 let effective = self.effective_domain_types(&[(schema, "")]);
541 with_keyword_append(&self.config.keyword_append_token, || {
542 with_deprecated_attrs(self.config.deprecated_attrs, || {
543 self.validate_header_values(schema)?;
544 self.validate_conversions(schema)?;
545 self.validate_paths()?;
546 let mut modules = GeneratedModuleSet::default();
547 let src = self.gen_schema(schema, &HashSet::new(), false, true, &effective)?;
548 modules.push(GeneratedModule {
549 path: format!("{}.rs", self.config.module_name),
550 source: src,
551 });
552 Ok(modules)
553 })
554 })
555 }
556
557 pub fn generate_multi(
569 &self,
570 schemas: &[(&Schema, &str)],
571 ) -> Result<GeneratedModuleSet, GenerateError> {
572 let effective = self.effective_domain_types(schemas);
573 with_keyword_append(&self.config.keyword_append_token, || {
574 with_deprecated_attrs(self.config.deprecated_attrs, || {
575 self.validate_paths()?;
576 self.generate_multi_inner(schemas, &effective)
577 })
578 })
579 }
580
581 fn generate_multi_inner(
582 &self,
583 schemas: &[(&Schema, &str)],
584 domain_types: &[(crate::ConversionSelector, String)],
585 ) -> Result<GeneratedModuleSet, GenerateError> {
586 let mut modules = GeneratedModuleSet::default();
587 let mut shared_types: HashSet<String> = HashSet::new();
588 let empty_set: HashSet<String> = HashSet::new();
589
590 {
592 let mut seen = HashSet::new();
593 for (i, (_, module_name)) in schemas.iter().enumerate() {
594 if !crate::config::is_valid_module_ident(module_name) {
595 return Err(GenerateError::InvalidConfiguration {
596 option: format!("schemas[{i}].module_name"),
597 value: module_name.to_string(),
598 reason:
599 "module name must be a single Rust identifier — no '/', '\\\\', '.', or '..'"
600 .into(),
601 });
602 }
603 if !seen.insert(module_name.to_string()) {
604 return Err(GenerateError::InvalidConfiguration {
605 option: format!("schemas[{i}].module_name"),
606 value: module_name.to_string(),
607 reason:
608 "duplicate module name — each schema must have a unique module name"
609 .into(),
610 });
611 }
612 }
613 }
614
615 if schemas.len() > 1 && self.config.shared_module.is_some() {
619 let owner_module = schemas[0].1.to_string();
620 let owner_byte_order = schemas[0].0.ir.byte_order;
621 let first_elements = partition_tokens(&schemas[0].0.ir.tokens);
622 for (schema, consumer_module) in schemas.iter().skip(1) {
623 let elements = partition_tokens(&schema.ir.tokens);
624 let consumer_byte_order = schema.ir.byte_order;
625 let check = |kind: &str, name: String, a: String, b: String| {
626 if a != b {
627 Err(GenerateError::IncompatibleSharedType {
628 name: name.clone(),
629 owner_module: owner_module.clone(),
630 consumer_module: consumer_module.to_string(),
631 difference: format!(
632 "{kind} fingerprint mismatch (owner={a}, consumer={b})"
633 ),
634 })
635 } else {
636 Ok(())
637 }
638 };
639 for et in &elements.enums {
641 let name = to_pascal_case(&et[0].name);
642 if let Some(ref_et) = first_elements
643 .enums
644 .iter()
645 .find(|e| to_pascal_case(&e[0].name) == name)
646 {
647 check(
648 "enum",
649 name,
650 canonical_token_fingerprint(ref_et, owner_byte_order),
651 canonical_token_fingerprint(et, consumer_byte_order),
652 )?;
653 }
654 }
655 for st in &elements.sets {
657 let name = to_pascal_case(&st[0].name);
658 if let Some(ref_st) = first_elements
659 .sets
660 .iter()
661 .find(|s| to_pascal_case(&s[0].name) == name)
662 {
663 check(
664 "set",
665 name,
666 canonical_token_fingerprint(ref_st, owner_byte_order),
667 canonical_token_fingerprint(st, consumer_byte_order),
668 )?;
669 }
670 }
671 for ct in &elements.composites {
673 let name = to_pascal_case(&ct[0].name);
674 if let Some(ref_ct) = first_elements
675 .composites
676 .iter()
677 .find(|c| to_pascal_case(&c[0].name) == name)
678 {
679 check(
680 "composite",
681 name,
682 canonical_token_fingerprint(ref_ct, owner_byte_order),
683 canonical_token_fingerprint(ct, consumer_byte_order),
684 )?;
685 }
686 }
687 }
688 }
689
690 if let Some(ref shared) = self.config.shared_module {
694 let owner = schemas[0].1;
695 if shared != owner {
696 return Err(GenerateError::InvalidConfiguration {
697 option: "shared_module".into(),
698 value: shared.clone(),
699 reason: format!(
700 "must equal the first schema module name (owner '{owner}'); \
701 consumers import `super::{shared}::*` from that module"
702 ),
703 });
704 }
705 }
706
707 {
711 let mut union_elements: Vec<(&Schema, crate::structured_ir::SchemaElements)> =
712 Vec::with_capacity(schemas.len());
713 for (schema, _) in schemas.iter() {
714 union_elements.push((schema, partition_tokens(&schema.ir.tokens)));
715 }
716 self.validate_conversions_union(&union_elements)?;
717 }
718
719 for (i, (schema, module_name)) in schemas.iter().enumerate() {
720 self.validate_header_values(schema)?;
721 if i == 0 {
722 let elements = partition_tokens(&schema.ir.tokens);
723 for et in &elements.enums {
724 let name = to_pascal_case(&et[0].name);
725 shared_types.insert(name.clone());
726 if let Some(warn) = warn_version_gated(&name, et, schema) {
727 modules.warnings.push(warn);
728 }
729 }
730 for st in &elements.sets {
731 shared_types.insert(to_pascal_case(&st[0].name));
732 }
733 for ct in &elements.composites {
734 let name = to_pascal_case(&ct[0].name);
735 shared_types.insert(name.clone());
736 if let Some(warn) = warn_version_gated(&name, ct, schema) {
737 modules.warnings.push(warn);
738 }
739 }
740 }
741 let is_importing = i > 0 && self.config.shared_module.is_some();
742 let emit_sbe_rt = i == 0 || self.config.shared_module.is_none();
745 let skip_set: &HashSet<String> = if self.config.shared_module.is_some() && i > 0 {
749 &shared_types
750 } else {
751 &empty_set
752 };
753 let src = self.gen_schema(schema, skip_set, is_importing, emit_sbe_rt, domain_types)?;
754 modules.push(GeneratedModule {
755 path: format!("{}.rs", module_name),
756 source: src,
757 });
758 }
759 Ok(modules)
760 }
761
762 fn build_enum_ctx<'s>(
764 tokens: &[crate::ir::Token],
765 schema: &'s crate::Schema,
766 ) -> crate::ItemContext<'s> {
767 let name = to_pascal_case(&tokens[0].name);
768 let encoding_type = tokens[0]
769 .encoding
770 .primitive_type
771 .unwrap_or(PrimitiveType::UInt8);
772 let et_str = rust_type(encoding_type).to_string();
773 let variants: Vec<_> = tokens
774 .iter()
775 .filter(|t| t.signal == crate::ir::Signal::Encoding)
776 .filter_map(|t| {
777 let val = t.encoding.constant_value.as_ref()?;
778 let value: i128 = if encoding_type == PrimitiveType::Char {
779 i128::from(val.as_bytes().first().copied().unwrap_or(0))
780 } else {
781 val.parse::<i128>().ok()?
784 };
785 Some(crate::EnumVariantInfo {
786 name: to_pascal_case(&t.name),
787 snake_name: to_snake_case(&t.name),
788 label: t.name.clone(),
789 value,
790 description: t.encoding.description.clone(),
791 })
792 })
793 .collect();
794 crate::ItemContext::Enum {
795 schema,
796 name,
797 encoding_type: et_str,
798 variants,
799 }
800 }
801
802 fn build_message_ctx<'s>(
804 msg: &MessageStructure,
805 kind: crate::ItemKind,
806 schema: &'s crate::Schema,
807 ) -> crate::ItemContext<'s> {
808 let name = to_pascal_case(&msg.name);
809 let name_with = |suffix: &str| format!("{name}{suffix}");
810 let fields = message_field_infos(&msg.fields, &[], None);
811 let name = match kind {
812 crate::ItemKind::MessageDecoder => name_with("Decoder"),
813 crate::ItemKind::MessageEncoder => name_with("Encoder"),
814 _ => name,
815 };
816 match kind {
817 crate::ItemKind::MessageDecoder => crate::ItemContext::MessageDecoder {
818 schema,
819 name,
820 template_id: msg.id,
821 block_length: msg.block_length,
822 fields,
823 },
824 crate::ItemKind::MessageEncoder => crate::ItemContext::MessageEncoder {
825 schema,
826 name,
827 template_id: msg.id,
828 block_length: msg.block_length,
829 fields,
830 },
831 _ => unreachable!("build_message_ctx only for MessageDecoder/MessageEncoder"),
832 }
833 }
834
835 fn build_composite_ctx<'s>(
842 tokens: &[crate::ir::Token],
843 schema: &'s crate::Schema,
844 ) -> crate::ItemContext<'s> {
845 use crate::structured_ir::MemberType;
846 let name = to_pascal_case(&tokens[0].name);
847 let member_field_token = |member_name: &str| {
854 tokens
855 .iter()
856 .find(|t| t.signal == crate::ir::Signal::BeginField && t.name == member_name)
857 };
858 let inner_type_token = |field_name: &str| {
859 let mut it = tokens.iter().skip_while(|t| {
862 !(t.signal == crate::ir::Signal::BeginField && t.name == field_name)
863 });
864 let _ = it.next(); it.find(|t| {
866 matches!(
867 t.signal,
868 crate::ir::Signal::Encoding
869 | crate::ir::Signal::BeginComposite
870 | crate::ir::Signal::BeginEnum
871 | crate::ir::Signal::BeginSet
872 )
873 })
874 };
875 let fields: Vec<_> = crate::structured_ir::parse_composite_members(tokens)
876 .into_iter()
877 .map(|m| {
878 let field_tok = member_field_token(&m.name);
879 let inner_tok = inner_type_token(&m.name);
880 let enc = match &m.member_type {
883 MemberType::Primitive { .. } => field_tok.map(|t| &t.encoding),
884 MemberType::Composite { .. }
885 | MemberType::Enum { .. }
886 | MemberType::Set { .. } => inner_tok.map(|t| &t.encoding),
887 };
888 let (rust_type, presence) = match &m.member_type {
889 MemberType::Primitive {
890 prim,
891 length,
892 presence,
893 ..
894 } => {
895 let base = crate::structured_ir::rust_type(*prim);
896 let rt = match length {
897 Some(len) => format!("[{base}; {len}]"),
898 None => base.to_string(),
899 };
900 let ps = match presence {
901 crate::ir::Presence::Optional => "optional",
902 crate::ir::Presence::Constant => "constant",
903 crate::ir::Presence::Required => "required",
904 };
905 (rt, ps)
906 }
907 MemberType::Composite { name, .. } => (to_pascal_case(name), "required"),
908 MemberType::Enum { name, .. } => (to_pascal_case(name), "required"),
909 MemberType::Set { name, .. } => (to_pascal_case(name), "required"),
910 };
911 crate::FieldInfo {
912 name: to_snake_case(&m.name),
913 rust_type,
914 offset: Some(m.offset),
915 since_version: m.since_version,
916 semantic_type: enc.and_then(|e| e.semantic_type.clone()),
917 presence,
918 null_value: enc.and_then(|e| e.null_value),
919 deprecated: enc.is_some_and(|e| e.deprecated),
920 description: enc.and_then(|e| e.description.clone()),
921 }
922 })
923 .collect();
924 crate::ItemContext::Composite {
925 schema,
926 name,
927 fields,
928 }
929 }
930
931 fn build_set_ctx<'s>(
933 tokens: &[crate::ir::Token],
934 schema: &'s crate::Schema,
935 ) -> crate::ItemContext<'s> {
936 let name = to_pascal_case(&tokens[0].name);
937 let encoding_type = tokens[0]
938 .encoding
939 .primitive_type
940 .unwrap_or(PrimitiveType::UInt8);
941 let et_str = rust_type(encoding_type).to_string();
942 let choices: Vec<_> = tokens
943 .iter()
944 .filter(|t| t.signal == crate::ir::Signal::Encoding)
945 .map(|t| crate::SetChoiceInfo {
946 name: to_pascal_case(&t.name),
947 snake_name: to_snake_case(&t.name),
948 label: t.name.clone(),
949 bit_position: t
950 .encoding
951 .constant_value
952 .as_ref()
953 .and_then(|v| v.parse::<u8>().ok())
954 .unwrap_or(0),
955 description: t.encoding.description.clone(),
956 })
957 .collect();
958 crate::ItemContext::Set {
959 schema,
960 name,
961 encoding_type: et_str,
962 choices,
963 }
964 }
965
966 fn run_hooks(&self, ctx: &crate::ItemContext, src: &mut String) {
968 if !self.config.has_hooks() {
969 return;
970 }
971 self.config.run_hooks(ctx, src);
972 }
973 fn gen_schema(
976 &self,
977 schema: &Schema,
978 shared: &HashSet<String>,
979 is_importing: bool,
980 emit_sbe_rt: bool,
981 domain_types: &[(crate::ConversionSelector, String)],
982 ) -> Result<String, GenerateError> {
983 let ir = &schema.ir;
984
985 let mut src = String::new();
986 writeln!(
992 src,
993 "/// Generated from SBE schema package `{}` id {} version {}.",
994 schema.package, schema.id, schema.version
995 )
996 .unwrap();
997 src.push_str(
1008 "#[allow(clippy::absurd_extreme_comparisons, clippy::double_must_use, \
1009 clippy::erasing_op, clippy::identity_op, clippy::unnecessary_cast)]\n",
1010 );
1011 src.push_str("#[allow(non_camel_case_types)]\n");
1012 src.push_str("#[allow(non_snake_case)]\n");
1013 src.push_str("#[allow(clippy::eq_op)]\n");
1014 src.push_str("#[allow(clippy::manual_range_contains)]\n\n");
1015
1016 if is_importing {
1019 if let Some(ref shared_mod) = self.config.shared_module {
1020 write!(src, "pub use super::{}::*;\n\n", shared_mod).unwrap();
1021 }
1022 }
1023
1024 let sealed_path = if let Some(ref ext) = self.config.external_sbe_rt_path {
1028 let owner = ext.strip_suffix("::sbe_rt").unwrap_or(ext);
1029 format!("{owner}::{}", crate::codegen::runtime::SEALED_MODULE)
1030 } else if is_importing {
1031 let shared = self
1032 .config
1033 .shared_module
1034 .as_deref()
1035 .expect("is_importing implies a shared module");
1036 format!(
1037 "super::{shared}::{}",
1038 crate::codegen::runtime::SEALED_MODULE
1039 )
1040 } else {
1041 crate::codegen::runtime::SEALED_MODULE.to_string()
1042 };
1043 crate::codegen::runtime::set_sealed_path(&sealed_path);
1044
1045 if let Some(ref ext) = self.config.external_sbe_rt_path {
1046 let _ = writeln!(src, "pub use {ext} as sbe_rt;\n");
1047 if self.config.has_conversions() {
1048 emit_conversion_traits(&mut src);
1049 }
1050 } else if emit_sbe_rt {
1051 src.push_str(&generate_sbe_rt_src());
1052 src.push_str(&crate::codegen::runtime::generate_sealed_module_src(
1057 self.config.shared_module.is_some(),
1058 ));
1059 if self.config.has_conversions() {
1060 emit_conversion_traits(&mut src);
1061 }
1062 }
1063
1064 let elements = partition_tokens(&ir.tokens);
1065
1066 for enum_tokens in &elements.enums {
1067 let type_name = to_pascal_case(&enum_tokens[0].name);
1068 if shared.contains(&type_name) {
1069 continue;
1070 }
1071 generate_enum(&mut src, enum_tokens);
1072 if self.config.has_hooks() {
1073 let ctx = Self::build_enum_ctx(enum_tokens, schema);
1074 self.run_hooks(&ctx, &mut src);
1075 }
1076 }
1077
1078 for set_tokens in &elements.sets {
1079 let type_name = to_pascal_case(&set_tokens[0].name);
1080 if shared.contains(&type_name) {
1081 continue;
1082 }
1083 generate_set(&mut src, set_tokens);
1084 if self.config.has_hooks() {
1085 let ctx = Self::build_set_ctx(set_tokens, schema);
1086 self.run_hooks(&ctx, &mut src);
1087 }
1088 }
1089
1090 for composite_tokens in &elements.composites {
1091 let type_name = to_pascal_case(&composite_tokens[0].name);
1092 if shared.contains(&type_name) {
1093 continue;
1094 }
1095 let comp_byte_order = ir.byte_order;
1096 generate_composite(&mut src, composite_tokens, comp_byte_order);
1097 if self.config.has_hooks() {
1098 let ctx = Self::build_composite_ctx(composite_tokens, schema);
1099 self.run_hooks(&ctx, &mut src);
1100 }
1101 }
1102
1103 let header_pascal = to_pascal_case(&ir.header_type);
1104 if header_pascal != "MessageHeader" && !shared.contains(&header_pascal) {
1105 write!(src, "pub type MessageHeader = {};\n\n", header_pascal).unwrap();
1106 }
1107
1108 let messages: Vec<MessageStructure> = elements
1109 .messages
1110 .iter()
1111 .map(|toks| parse_message_structure(toks, &elements))
1112 .collect();
1113
1114 let mut conv_sels = self.config.conversions.clone();
1117 for (sel, _) in domain_types {
1118 if !conv_sels.iter().any(|s| s == sel) {
1119 conv_sels.push(sel.clone());
1120 }
1121 }
1122
1123 let mut schema_markers = occupied_type_names(&elements);
1124 let mut message_markers: Vec<(String, String)> = Vec::new();
1125 for msg in &messages {
1126 let multi = messages.len() > 1;
1127 let (decoder_ts, marker) = generate_message_decoder(
1128 msg,
1129 &elements,
1130 &mut schema_markers,
1131 ir.byte_order,
1132 ir.id,
1133 ir.version,
1134 &ir.header_type,
1135 &ir.package,
1136 multi,
1137 self.config.enable_display_debug,
1138 self.config.enable_meta_attributes,
1139 self.config.enable_dispatch,
1140 self.config.domain_objects,
1141 self.config.domain_var_data,
1142 &conv_sels,
1143 domain_types,
1144 &self.config.hooks,
1145 schema,
1146 &self.config.null_as_option,
1147 self.config.all_enums_as_option,
1148 );
1149 src.push_str(&decoder_ts.to_string());
1150 src.push('\n');
1151 message_markers.push((to_pascal_case(&msg.name), marker));
1152 if self.config.has_hooks() {
1154 let ctx = Self::build_message_ctx(msg, crate::ItemKind::MessageDecoder, schema);
1155 self.run_hooks(&ctx, &mut src);
1156 }
1157 let encoder_ts = generate_message_encoder(
1158 msg,
1159 &elements,
1160 ir.byte_order,
1161 ir.id,
1162 ir.version,
1163 &ir.header_type,
1164 multi,
1165 &conv_sels,
1166 domain_types,
1167 self.config.enable_meta_attributes,
1168 self.config.enable_display_debug,
1169 );
1170 src.push_str(&encoder_ts.to_string());
1171 if self.config.has_hooks() {
1173 let ctx = Self::build_message_ctx(msg, crate::ItemKind::MessageEncoder, schema);
1174 self.run_hooks(&ctx, &mut src);
1175 }
1176
1177 if !conv_sels.is_empty() {
1179 let manual_impl_snippets = generate_manual_impl_snippets(
1180 &elements,
1181 domain_types,
1182 &self.config.manual_impl_selectors,
1183 );
1184 let converter_ts = generate_converter_impls(
1185 msg,
1186 &conv_sels,
1187 domain_types,
1188 &manual_impl_snippets,
1189 multi,
1190 );
1191 src.push_str(&converter_ts);
1192 }
1193 src.push('\n');
1194 if self.config.enable_meta_attributes {
1195 generate_message_field_meta(&mut src, msg);
1196 }
1197 }
1198
1199 if self.config.has_conversions() && emit_sbe_rt {
1207 let impl_blocks = generate_conversion_impl_blocks(
1208 &elements,
1209 &self.config.conversions,
1210 domain_types,
1211 &self.config.manual_impl_selectors,
1212 );
1213 src.push_str(&impl_blocks);
1214 }
1215
1216 {
1218 let has_staged = messages.iter().any(|m| {
1219 matches!(
1220 encoded_length::strategy(m),
1221 encoded_length::LengthStrategy::Staged
1222 )
1223 });
1224 if has_staged {
1225 let support_ts = encoded_length::generate_support();
1226 src.push_str(&support_ts.to_string());
1227 }
1228 }
1229
1230 if let Some(ref sem_ver) = schema.ir.semantic_version {
1232 write!(
1233 src,
1234 "pub const SEMANTIC_VERSION: &str = \"{}\";\n\n",
1235 sem_ver
1236 )
1237 .unwrap();
1238 }
1239 let schema_hash = compute_schema_hash(&schema.package, schema.id, schema.version);
1240 write!(src, "pub const SCHEMA_HASH: u64 = {};\n\n", schema_hash).unwrap();
1241 let sha256_hash = compute_schema_sha256(&schema.ir);
1242 src.push_str("pub const SCHEMA_SHA256: [u8; 32] = [");
1243 for (i, &b) in sha256_hash.iter().enumerate() {
1244 if i > 0 {
1245 src.push_str(", ");
1246 }
1247 write!(src, "0x{:02x}", b).unwrap();
1248 }
1249 src.push_str("];\n\n");
1250 let hex: String = sha256_hash.iter().map(|b| format!("{:02x}", b)).collect();
1251 write!(src, "pub const SCHEMA_SHA256_HEX: &str = \"{}\";\n\n", hex).unwrap();
1252 generate_prelude(
1254 &mut src,
1255 &elements,
1256 &messages,
1257 ir.id,
1258 ir.version,
1259 self.config.enable_dispatch,
1260 );
1261 if let Some(ref err_path) = self.config.error_from_path {
1263 let err_ty: syn::Type = syn::parse_str(err_path).expect("invalid error_from_path");
1264 let span = proc_macro2::Span::call_site();
1265 let impls = quote::quote! {
1266 impl From<sbe_rt::EncodeError> for #err_ty {
1268 fn from(e: sbe_rt::EncodeError) -> Self {
1269 Self::from(format!("sbe encode: {e}"))
1270 }
1271 }
1272 impl From<sbe_rt::DecodeError> for #err_ty {
1274 fn from(e: sbe_rt::DecodeError) -> Self {
1275 Self::from(format!("sbe decode: {e}"))
1276 }
1277 }
1278 };
1279 src.push_str(&impls.to_string());
1280 src.push('\n');
1281 }
1282 let read_bytes_ts: proc_macro2::TokenStream = quote::quote! {
1286 #[inline]
1291 pub fn read_bytes<const N: usize>(buf: &[u8], offset: usize) -> [u8; N] {
1292 buf[offset..offset + N].try_into().expect("read_bytes: buffer too short")
1293 }
1294
1295 #[inline]
1296 pub fn write_bytes<const N: usize>(buf: &mut [u8], offset: usize, bytes: &[u8; N]) {
1297 buf[offset..offset + N].copy_from_slice(bytes);
1298 }
1299
1300 #[inline(always)]
1308 #[allow(dead_code)] unsafe fn read_bytes_unchecked<const N: usize>(buf: &[u8], offset: usize) -> [u8; N] {
1310 unsafe {
1312 core::ptr::read_unaligned(buf.as_ptr().add(offset) as *const [u8; N])
1313 }
1314 }
1315
1316
1317 #[inline]
1323 #[allow(dead_code)]
1324 unsafe fn write_bytes_unchecked<const N: usize>(
1325 buf: &mut [u8],
1326 offset: usize,
1327 bytes: &[u8; N],
1328 ) {
1329 unsafe {
1331 core::ptr::write_unaligned(buf.as_mut_ptr().add(offset) as *mut [u8; N], *bytes)
1332 }
1333 }
1334 };
1335 src.push_str(&read_bytes_ts.to_string());
1336 src.push('\n');
1337 generate_schema_id_from_header(&mut src, &elements, &ir.header_type, ir.byte_order);
1338
1339 if self.config.enable_dispatch {
1340 let any_msg_ts = generate_any_message(
1341 &messages,
1342 &elements,
1343 ir.id,
1344 &ir.header_type,
1345 &ir.package,
1346 &message_markers,
1347 );
1348 src.push_str(&any_msg_ts.to_string());
1349 src.push('\n');
1350 }
1351
1352 let mut file = match syn::parse_str::<syn::File>(&src) {
1353 Ok(f) => f,
1354 Err(e) => {
1355 return Err(GenerateError::InvalidGeneratedSource {
1356 module: self.config.module_name.clone(),
1357 error: e.to_string(),
1358 });
1359 }
1360 };
1361 annotate_missing_public_docs(&mut file);
1362 Ok(prettyplease::unparse(&file))
1363 }
1364}
1365
1366fn item_is_public(vis: &syn::Visibility) -> bool {
1367 matches!(vis, syn::Visibility::Public(_))
1368}
1369
1370fn attrs_have_doc(attrs: &[syn::Attribute]) -> bool {
1371 attrs.iter().any(|a| a.path().is_ident("doc"))
1372}
1373
1374fn fallback_public_doc(kind: &str, name: &str) -> syn::Attribute {
1375 let text = format!("Generated {kind} `{name}`.");
1376 syn::parse_quote!(#[doc = #text])
1377}
1378
1379fn doc_is_placeholder(attr: &syn::Attribute) -> bool {
1380 if !attr.path().is_ident("doc") {
1381 return false;
1382 }
1383 let syn::Meta::NameValue(nv) = &attr.meta else {
1384 return false;
1385 };
1386 let syn::Expr::Lit(syn::ExprLit {
1387 lit: syn::Lit::Str(s),
1388 ..
1389 }) = &nv.value
1390 else {
1391 return false;
1392 };
1393 s.value().trim() == "Generated public API."
1394}
1395
1396fn ensure_public_doc(attrs: &mut Vec<syn::Attribute>, kind: &str, name: &str) {
1397 if attrs.iter().any(doc_is_placeholder)
1398 && attrs
1399 .iter()
1400 .filter(|a| a.path().is_ident("doc"))
1401 .all(doc_is_placeholder)
1402 {
1403 attrs.retain(|a| !doc_is_placeholder(a));
1404 }
1405 if !attrs_have_doc(attrs) {
1406 attrs.insert(0, fallback_public_doc(kind, name));
1407 }
1408}
1409
1410fn annotate_missing_public_docs(file: &mut syn::File) {
1414 for item in &mut file.items {
1415 annotate_item(item);
1416 }
1417}
1418
1419fn annotate_item(item: &mut syn::Item) {
1420 match item {
1421 syn::Item::Struct(s) if item_is_public(&s.vis) => {
1422 let name = s.ident.to_string();
1423 ensure_public_doc(&mut s.attrs, "struct", &name);
1424 for field in &mut s.fields {
1425 if !item_is_public(&field.vis) {
1426 continue;
1427 }
1428 match &field.ident {
1429 Some(ident) => ensure_public_doc(&mut field.attrs, "field", &ident.to_string()),
1430 None => {
1431 }
1434 }
1435 }
1436 }
1437 syn::Item::Enum(e) if item_is_public(&e.vis) => {
1438 let name = e.ident.to_string();
1439 ensure_public_doc(&mut e.attrs, "enum", &name);
1440 for variant in &mut e.variants {
1441 ensure_public_doc(&mut variant.attrs, "variant", &variant.ident.to_string());
1442 for field in &mut variant.fields {
1443 if let Some(ident) = &field.ident {
1444 ensure_public_doc(&mut field.attrs, "field", &ident.to_string());
1445 }
1446 }
1447 }
1448 }
1449 syn::Item::Fn(f) if item_is_public(&f.vis) => {
1450 ensure_public_doc(&mut f.attrs, "function", &f.sig.ident.to_string());
1451 }
1452 syn::Item::Const(c) if item_is_public(&c.vis) => {
1453 ensure_public_doc(&mut c.attrs, "constant", &c.ident.to_string());
1454 }
1455 syn::Item::Type(t) if item_is_public(&t.vis) => {
1456 ensure_public_doc(&mut t.attrs, "type", &t.ident.to_string());
1457 }
1458 syn::Item::Trait(t) if item_is_public(&t.vis) => {
1459 let name = t.ident.to_string();
1460 ensure_public_doc(&mut t.attrs, "trait", &name);
1461 for trait_item in &mut t.items {
1462 match trait_item {
1463 syn::TraitItem::Fn(f) => {
1464 ensure_public_doc(&mut f.attrs, "method", &f.sig.ident.to_string());
1465 }
1466 syn::TraitItem::Const(c) => {
1467 ensure_public_doc(&mut c.attrs, "constant", &c.ident.to_string());
1468 }
1469 syn::TraitItem::Type(ty) => {
1470 ensure_public_doc(&mut ty.attrs, "type", &ty.ident.to_string());
1471 }
1472 _ => {}
1473 }
1474 }
1475 }
1476 syn::Item::Impl(i) => {
1477 for impl_item in &mut i.items {
1478 match impl_item {
1479 syn::ImplItem::Fn(f) if item_is_public(&f.vis) => {
1480 ensure_public_doc(&mut f.attrs, "method", &f.sig.ident.to_string());
1481 }
1482 syn::ImplItem::Const(c) if item_is_public(&c.vis) => {
1483 ensure_public_doc(&mut c.attrs, "constant", &c.ident.to_string());
1484 }
1485 syn::ImplItem::Type(t) if item_is_public(&t.vis) => {
1486 ensure_public_doc(&mut t.attrs, "type", &t.ident.to_string());
1487 }
1488 _ => {}
1489 }
1490 }
1491 }
1492 syn::Item::Mod(m) if item_is_public(&m.vis) => {
1493 ensure_public_doc(&mut m.attrs, "module", &m.ident.to_string());
1494 if let Some((_, items)) = &mut m.content {
1495 for nested in items {
1496 annotate_item(nested);
1497 }
1498 }
1499 }
1500 syn::Item::Use(u) if item_is_public(&u.vis) => {
1501 ensure_public_doc(&mut u.attrs, "import", "use");
1502 }
1503 syn::Item::Static(s) if item_is_public(&s.vis) => {
1504 ensure_public_doc(&mut s.attrs, "static", &s.ident.to_string());
1505 }
1506 _ => {}
1507 }
1508}
1509
1510#[cfg(test)]
1513mod tests {
1514 use super::Generator;
1515 use crate::{GenerationConfig, Schema};
1516
1517 #[test]
1518 fn generator_emits_deterministic_module_name() -> Result<(), Box<dyn std::error::Error>> {
1519 let mut generator = Generator::new(GenerationConfig::new("market_data"));
1520 let schema = Schema::new("fix.sbe", 1, 0);
1521
1522 let modules = generator.generate(&schema)?;
1523 let collected = modules.modules().collect::<Vec<_>>();
1524
1525 assert_eq!(collected.len(), 1);
1526 assert_eq!(collected[0].path, "market_data.rs");
1527 assert!(collected[0].source.contains("fix.sbe"));
1528
1529 Ok(())
1530 }
1531
1532 #[test]
1533 fn generate_multi_creates_separate_modules() -> Result<(), Box<dyn std::error::Error>> {
1534 let mut config = GenerationConfig::new("common");
1535 config.shared_module = Some("common_types".to_string());
1536
1537 let mut generator = Generator::new(config);
1538
1539 let schema_a = Schema::new("common.sbe", 1, 0);
1540 let schema_b = Schema::new("market_data.sbe", 2, 0);
1541
1542 let modules =
1543 generator.generate_multi(&[(&schema_a, "common_types"), (&schema_b, "market_data")])?;
1544 let collected: Vec<_> = modules.modules().collect();
1545
1546 assert_eq!(collected.len(), 2);
1547 assert_eq!(collected[0].path, "common_types.rs");
1548 assert_eq!(collected[1].path, "market_data.rs");
1549
1550 assert!(collected[0].source.contains("pub mod sbe_rt"));
1551
1552 assert!(!collected[1].source.contains("pub mod sbe_rt"));
1554
1555 assert!(
1556 collected[1]
1557 .source
1558 .contains("pub use super::common_types::*;")
1559 );
1560
1561 assert!(collected[0].source.contains("common.sbe"));
1562 assert!(collected[1].source.contains("market_data.sbe"));
1563
1564 Ok(())
1565 }
1566
1567 #[test]
1568 fn into_parts_preserves_module_order_and_warnings() -> Result<(), Box<dyn std::error::Error>> {
1569 let mut set = super::GeneratedModuleSet::default();
1570 set.push(super::GeneratedModule {
1571 path: "common_types.rs".into(),
1572 source: "mod common;".into(),
1573 });
1574 set.push(super::GeneratedModule {
1575 path: "market_data.rs".into(),
1576 source: "mod market;".into(),
1577 });
1578 set.warnings
1579 .push("shared type Price has sinceVersion > 0".into());
1580 let expected_paths: Vec<String> = set.modules().map(|m| m.path.clone()).collect();
1581 let expected_warnings = set.warnings().to_vec();
1582 let (modules, warnings) = set.into_parts();
1583 assert_eq!(
1584 modules.iter().map(|m| m.path.as_str()).collect::<Vec<_>>(),
1585 expected_paths
1586 .iter()
1587 .map(String::as_str)
1588 .collect::<Vec<_>>()
1589 );
1590 assert_eq!(modules[0].source, "mod common;");
1591 assert_eq!(modules[1].source, "mod market;");
1592 assert_eq!(warnings, expected_warnings);
1593 Ok(())
1594 }
1595
1596 #[test]
1597 fn generate_multi_without_shared_module_emits_sbe_rt_everywhere()
1598 -> Result<(), Box<dyn std::error::Error>> {
1599 let config = GenerationConfig::new("common");
1600 let mut generator = Generator::new(config);
1601
1602 let schema_a = Schema::new("common.sbe", 1, 0);
1603 let schema_b = Schema::new("market_data.sbe", 2, 0);
1604
1605 let modules = generator.generate_multi(&[(&schema_a, "a_mod"), (&schema_b, "b_mod")])?;
1606 let collected: Vec<_> = modules.modules().collect();
1607
1608 assert_eq!(collected.len(), 2);
1609
1610 assert!(collected[0].source.contains("pub mod sbe_rt"));
1612 assert!(collected[1].source.contains("pub mod sbe_rt"));
1613
1614 assert!(!collected[1].source.contains("\npub use super::"));
1616 Ok(())
1617 }
1618
1619 use super::{
1620 SchemaElements, parse_composite_members, parse_field_structure, parse_group_structure,
1621 parse_message_structure, parse_vardata_structure, to_snake_case,
1622 };
1623 use crate::ir::{Encoding, Signal, Token};
1624
1625 fn make_token(signal: Signal) -> Token {
1626 Token {
1627 id: None,
1628 name: String::new(),
1629 signal,
1630 encoding: Encoding::default(),
1631 span: None,
1632 }
1633 }
1634
1635 fn empty_elements() -> SchemaElements {
1636 SchemaElements {
1637 composites: vec![],
1638 enums: vec![],
1639 sets: vec![],
1640 messages: vec![],
1641 }
1642 }
1643
1644 #[test]
1645 fn message_structure_skips_unexpected_signal() -> Result<(), Box<dyn std::error::Error>> {
1646 let elem = empty_elements();
1649 let _ = parse_message_structure(
1650 &[
1651 make_token(Signal::BeginMessage),
1652 make_token(Signal::BeginEnum), make_token(Signal::EndMessage),
1654 ],
1655 &elem,
1656 );
1657
1658 Ok(())
1659 }
1660
1661 #[test]
1662 fn group_structure_skips_unexpected_signal() -> Result<(), Box<dyn std::error::Error>> {
1663 let elem = empty_elements();
1666 let _ = parse_group_structure(
1667 &[
1668 make_token(Signal::BeginGroup),
1669 make_token(Signal::BeginMessage), make_token(Signal::EndGroup),
1671 ],
1672 &elem,
1673 );
1674
1675 Ok(())
1676 }
1677
1678 #[test]
1679 fn vardata_structure_skips_non_length_fields() -> Result<(), Box<dyn std::error::Error>> {
1680 let _ = parse_vardata_structure(&[
1683 make_token(Signal::BeginComposite),
1684 make_token(Signal::BeginField),
1685 make_token(Signal::EndField),
1686 make_token(Signal::EndComposite),
1687 ]);
1688
1689 Ok(())
1690 }
1691
1692 #[test]
1693 fn composite_members_skips_non_field_signals() -> Result<(), Box<dyn std::error::Error>> {
1694 let _ = parse_composite_members(&[
1697 make_token(Signal::BeginComposite),
1698 make_token(Signal::BeginMessage), make_token(Signal::EndComposite),
1700 ]);
1701 Ok(())
1702 }
1703
1704 #[test]
1705 fn field_structure_falls_back_to_uint8_primitive() -> Result<(), Box<dyn std::error::Error>> {
1706 let elem = empty_elements();
1709 let _ = parse_field_structure(
1710 &[
1711 make_token(Signal::BeginField),
1712 make_token(Signal::BeginMessage), make_token(Signal::EndField),
1714 ],
1715 &elem,
1716 );
1717
1718 Ok(())
1719 }
1720
1721 #[test]
1722 fn group_array_codegen_uses_the_complete_field_extent_and_element_range()
1723 -> Result<(), Box<dyn std::error::Error>> {
1724 let xml = r#"<?xml version="1.0"?>
1725 <messageSchema package="array.guard" id="305" version="1" byteOrder="littleEndian">
1726 <types>
1727 <composite name="messageHeader">
1728 <type name="blockLength" primitiveType="uint16"/>
1729 <type name="templateId" primitiveType="uint16"/>
1730 <type name="schemaId" primitiveType="uint16"/>
1731 <type name="version" primitiveType="uint16"/>
1732 </composite>
1733 <composite name="groupSizeEncoding">
1734 <type name="blockLength" primitiveType="uint16"/>
1735 <type name="numInGroup" primitiveType="uint16"/>
1736 </composite>
1737 <type name="Values" primitiveType="uint32" length="2"/>
1738 <enum name="State" encodingType="uint8">
1739 <validValue name="Ready">1</validValue>
1740 </enum>
1741 <set name="Flags" encodingType="uint8">
1742 <choice name="Active">0</choice>
1743 </set>
1744 <enum name="BooleanType" encodingType="uint8">
1745 <validValue name="F">0</validValue>
1746 <validValue name="T">1</validValue>
1747 </enum>
1748 </types>
1749 <message name="ArrayBoundaryMessage" id="1">
1750 <group name="entries" id="1">
1751 <field name="base" id="2" type="uint8"/>
1752 <field name="values" id="3" type="Values"/>
1753 <field name="state" id="4" type="State" sinceVersion="1"/>
1754 <field name="flags" id="5" type="Flags" sinceVersion="1"/>
1755 <field name="enabled" id="6" type="BooleanType" sinceVersion="1"/>
1756 </group>
1757 </message>
1758 </messageSchema>"#;
1759 let schema = crate::Schema::from_ir(crate::parse(xml)?);
1760 let mut generator = crate::Generator::new(crate::GenerationConfig::new("array_guard"));
1761 let modules = generator.generate(&schema)?;
1762 let source = &modules
1763 .modules()
1764 .next()
1765 .ok_or("missing generated module")?
1766 .source;
1767
1768 assert!(
1769 source.contains("9 > self.acting_block_length"),
1770 "u32[2] at offset 1 must require all nine entry bytes"
1771 );
1772 assert!(
1773 source.contains("let all: [u8; 8]"),
1774 "u32[2] must bulk-read exactly eight bytes"
1775 );
1776 assert!(
1777 source.contains("all[0usize]") && source.contains("all[7usize]"),
1778 "the unrolled array decode must use the complete byte range"
1779 );
1780 assert!(
1781 source.contains("|| 10 > self.acting_block_length"),
1782 "the versioned enum at offset nine must require its complete tenth byte"
1783 );
1784 assert!(
1785 source.contains("|| 11 > self.acting_block_length"),
1786 "the versioned set at offset ten must require its complete eleventh byte"
1787 );
1788 assert!(
1789 source.contains("pub fn try_enabled_bool(&self) -> Result<Option<bool>,")
1790 && source.contains("InvalidBoolean"),
1791 "a versioned BooleanType group field must carry the typed bool accessor"
1792 );
1793 Ok(())
1794 }
1795
1796 #[test]
1797 fn snake_case_handles_empty_or_special_input() -> Result<(), Box<dyn std::error::Error>> {
1798 assert_eq!(to_snake_case(""), "");
1799 assert_eq!(to_snake_case("Foo__Bar"), "foo_bar");
1801
1802 Ok(())
1803 }
1804
1805 #[test]
1806 fn partition_skips_unexpected_at_top_level() -> Result<(), Box<dyn std::error::Error>> {
1807 let _ = super::partition_tokens(&[make_token(Signal::BeginField)]);
1810
1811 Ok(())
1812 }
1813
1814 #[test]
1815 fn partition_skips_unexpected_in_message_body() -> Result<(), Box<dyn std::error::Error>> {
1816 let _ = super::partition_tokens(&[
1819 make_token(Signal::BeginMessage),
1820 make_token(Signal::BeginEnum), make_token(Signal::EndMessage),
1822 ]);
1823
1824 Ok(())
1825 }
1826
1827 #[test]
1828 fn partition_skips_unexpected_in_group_body() -> Result<(), Box<dyn std::error::Error>> {
1829 let _ = super::partition_tokens(&[
1832 make_token(Signal::BeginGroup),
1833 make_token(Signal::BeginMessage), make_token(Signal::EndGroup),
1835 ]);
1836
1837 Ok(())
1838 }
1839
1840 #[test]
1841 fn partition_skips_unexpected_after_top_level_items() -> Result<(), Box<dyn std::error::Error>>
1842 {
1843 let _ = super::partition_tokens(&[
1845 make_token(Signal::BeginMessage),
1846 make_token(Signal::EndMessage),
1847 make_token(Signal::BeginEnum), ]);
1849
1850 Ok(())
1851 }
1852
1853 #[test]
1854 fn semantic_type_matches_primitive_field() -> Result<(), Box<dyn std::error::Error>> {
1855 use crate::ir::{Presence, PrimitiveType};
1856 use crate::structured_ir::{FieldType, MessageField};
1857 let field = MessageField {
1858 name: "exchangeTimestamp".into(),
1859 id: Some(1),
1860 offset: 0,
1861 presence: Presence::Required,
1862 since_version: 0,
1863 null_value: None,
1864 min_value: None,
1865 max_value: None,
1866 description: None,
1867 deprecated: false,
1868 semantic_type: Some("UTCTimestamp".into()),
1869 constant_value: None,
1870 epoch: None,
1871 time_unit: None,
1872 character_encoding: None,
1873 field_type: FieldType::Primitive(PrimitiveType::UInt64, None),
1874 };
1875 let conversions = vec![crate::ConversionSelector::semantic_type("UTCTimestamp")];
1876 assert!(
1877 super::field_has_conversion_free(&field, &conversions),
1878 "SemanticType should match primitive u64 with semanticType=UTCTimestamp"
1879 );
1880
1881 let domain_types = vec![(
1882 crate::ConversionSelector::semantic_type("UTCTimestamp"),
1883 "chrono::DateTime<chrono::Utc>".into(),
1884 )];
1885 let dt = super::find_domain_type(&field, &domain_types);
1886 assert_eq!(
1887 dt,
1888 Some("chrono::DateTime<chrono::Utc>"),
1889 "should find domain type for UTCTimestamp"
1890 );
1891 Ok(())
1892 }
1893
1894 #[test]
1895 fn chrono_converter_generates_accessor() -> Result<(), Box<dyn std::error::Error>> {
1896 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1897 <sbe:messageSchema xmlns:sbe="http://fixprotocol.io/2016/sbe"
1898 package="test.chrono" id="1" version="0" byteOrder="littleEndian">
1899 <types>
1900 <composite name="messageHeader">
1901 <type name="blockLength" primitiveType="uint16"/>
1902 <type name="templateId" primitiveType="uint16"/>
1903 <type name="schemaId" primitiveType="uint16"/>
1904 <type name="version" primitiveType="uint16"/>
1905 </composite>
1906 </types>
1907 <sbe:message name="TsMsg" id="1">
1908 <field name="ts" id="1" type="uint64" semanticType="UTCTimestamp"/>
1909 </sbe:message>
1910 </sbe:messageSchema>"#;
1911 let ir = crate::parse(xml)?;
1912 let schema = crate::Schema::from_ir(ir);
1913 let config = crate::GenerationConfig::new("test_chrono").with_domain_type(
1914 crate::ConversionSelector::semantic_type("UTCTimestamp"),
1915 "chrono::DateTime<chrono::Utc>",
1916 );
1917 let mut generator = crate::Generator::new(config);
1918 let modules = generator.generate(&schema)?;
1919 let src = modules.modules().next().unwrap().source.clone();
1920 assert!(
1921 src.contains("fn try_ts(") && src.contains("chrono::DateTime"),
1922 "should generate fallible concrete DateTime accessor for UTCTimestamp field"
1923 );
1924 assert!(
1925 src.contains("fn ts_wire"),
1926 "should rename raw u64 getter to _wire"
1927 );
1928 assert!(
1929 src.contains("impl TryFromSbe<u64> for chrono::DateTime<chrono::Utc>"),
1930 "should generate TryFromSbe impl"
1931 );
1932 Ok(())
1933 }
1934
1935 #[test]
1936 fn narrow_message_header_rejects_values_above_declared_field_maximum() {
1937 fn generate(xml: &str) -> Result<super::GeneratedModuleSet, super::GenerateError> {
1938 let ir = crate::parse(xml).expect("schema should parse before codegen validation");
1939 let schema = crate::Schema::from_ir(ir);
1940 crate::Generator::new(crate::GenerationConfig::new("narrow")).generate(&schema)
1941 }
1942
1943 fn schema(schema_id: u16, version: u16, template_id: u16, block_length: u16) -> String {
1944 format!(
1945 r#"<messageSchema package="test" id="{schema_id}" version="{version}" byteOrder="littleEndian">
1946 <types>
1947 <composite name="messageHeader">
1948 <type name="schemaId" primitiveType="uint8"/>
1949 <type name="version" primitiveType="uint8"/>
1950 <type name="templateId" primitiveType="uint8"/>
1951 <type name="blockLength" primitiveType="uint8"/>
1952 </composite>
1953 </types>
1954 <message name="M" id="{template_id}" blockLength="{block_length}"/>
1955 </messageSchema>"#
1956 )
1957 }
1958
1959 for (xml, field) in [
1960 (schema(255, 1, 1, 0), "schemaId"),
1961 (schema(1, 255, 1, 0), "version"),
1962 (schema(1, 1, 255, 0), "templateId"),
1963 (schema(1, 1, 1, 255), "blockLength"),
1964 ] {
1965 let error = generate(&xml).expect_err("reserved null/max value must be rejected");
1966 assert!(
1967 error.to_string().contains(field),
1968 "expected {field} error, got: {error}"
1969 );
1970 }
1971 }
1972
1973 #[test]
1976 fn field_named_remaining_keeps_name_placement_on_metadata()
1977 -> Result<(), Box<dyn std::error::Error>> {
1978 let xml = r#"<messageSchema package="test" id="1" version="1" byteOrder="littleEndian">
1979 <types>
1980 <composite name="messageHeader">
1981 <type name="blockLength" primitiveType="uint16"/>
1982 <type name="templateId" primitiveType="uint16"/>
1983 <type name="schemaId" primitiveType="uint16"/>
1984 <type name="version" primitiveType="uint16"/>
1985 </composite>
1986 </types>
1987 <message name="Msg" id="1" blockLength="8">
1988 <field name="remaining" id="1" type="int64"/>
1989 </message>
1990 </messageSchema>"#;
1991
1992 let ir = crate::parse(xml).expect("schema should parse");
1993 let schema = crate::Schema::from_ir(ir);
1994 let modules =
1995 crate::Generator::new(crate::GenerationConfig::new("test")).generate(&schema)?;
1996 let src = modules.modules().next().expect("one module").source.clone();
1997
1998 assert!(
1999 src.contains("fn remaining(&self) -> i64")
2000 || src.contains("fn remaining(&self) -> i64,"),
2001 "field accessor must keep name remaining() as i64. src snippet check failed"
2002 );
2003 assert!(
2004 !src.contains("fn remaining_field"),
2005 "placement-name fields must not be renamed to remaining_field"
2006 );
2007 assert!(
2008 src.contains("fn get_metadata("),
2009 "placement utils must be on get_metadata()"
2010 );
2011 assert!(
2013 src.contains("DecoderMetadata"),
2014 "DecoderMetadata type must be emitted"
2015 );
2016
2017 Ok(())
2018 }
2019
2020 #[test]
2024 fn hook_adds_serde_impls_for_enum_and_set() -> Result<(), Box<dyn std::error::Error>> {
2025 let xml = r#"<messageSchema package="test" id="1" version="0" byteOrder="littleEndian">
2026 <types>
2027 <composite name="messageHeader">
2028 <type name="blockLength" primitiveType="uint16"/>
2029 <type name="templateId" primitiveType="uint16"/>
2030 <type name="schemaId" primitiveType="uint16"/>
2031 <type name="version" primitiveType="uint16"/>
2032 </composite>
2033 <enum name="EventCode" encodingType="uint32">
2034 <validValue name="Ok" description="Success">200</validValue>
2035 <validValue name="Error" description="Failure">400</validValue>
2036 <validValue name="Timeout">408</validValue>
2037 </enum>
2038 <set name="OptionalFields" encodingType="uint8">
2039 <choice name="hasPrice">0</choice>
2040 <choice name="hasQty">1</choice>
2041 <choice name="hasVenue">2</choice>
2042 </set>
2043 </types>
2044 <message name="Msg" id="1" blockLength="0"/>
2045 </messageSchema>"#;
2046
2047 use crate::{EnumVariantInfo, ItemContext, ItemKind, SetChoiceInfo};
2048 use quote::format_ident;
2049
2050 let config = crate::GenerationConfig::new("test")
2051 .with_hook(|ctx: &ItemContext| -> Vec<proc_macro2::TokenStream> {
2052 match ctx {
2053 ItemContext::Enum { name, variants, .. } => {
2054 let ident = format_ident!("{name}");
2055 let var_names: Vec<_> = variants.iter().map(|v| format_ident!("{}", v.name)).collect();
2056 let var_labels: Vec<_> = variants.iter().map(|v| v.name.clone()).collect();
2057 vec![quote::quote! {
2058 impl serde::Serialize for #ident {
2059 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
2060 let label = match self {
2061 #(Self::#var_names => #var_labels,)*
2062 };
2063 s.serialize_str(label)
2064 }
2065 }
2066
2067 impl<'de> serde::Deserialize<'de> for #ident {
2068 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
2069 let s = <&str>::deserialize(d)?;
2070 match s {
2071 #(#var_labels => Ok(Self::#var_names),)*
2072 _ => Err(serde::de::Error::unknown_variant(s, &[#(#var_labels),*])),
2073 }
2074 }
2075 }
2076 }]
2077 }
2078 ItemContext::Set { name, encoding_type, choices, .. } => {
2079 let ident = format_ident!("{name}");
2080 let c_getters: Vec<_> = choices
2085 .iter()
2086 .map(|c| format_ident!("is_{}", c.snake_name))
2087 .collect();
2088 let c_labels: Vec<_> = choices.iter().map(|c| c.label.clone()).collect();
2089 let c_bits: Vec<_> = choices.iter().map(|c| c.bit_position).collect();
2090 let acc_ty: syn::Type = syn::parse_str(encoding_type).unwrap();
2091 vec![quote::quote! {
2092 impl serde::Serialize for #ident {
2093 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
2094 let mut names = Vec::new();
2095 #(if self.#c_getters() { names.push(#c_labels); })*
2096 names.serialize(s)
2097 }
2098 }
2099
2100 impl<'de> serde::Deserialize<'de> for #ident {
2101 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
2102 let names: Vec<String> = Vec::deserialize(d)?;
2103 let mut value: u64 = 0;
2104 for name in &names {
2105 match name.as_str() {
2106 #(#c_labels => value |= 1u64 << #c_bits,)*
2107 other => return Err(serde::de::Error::unknown_variant(
2108 other, &[#(#c_labels),*])),
2109 }
2110 }
2111 Ok(Self(value as #acc_ty))
2112 }
2113 }
2114 }]
2115 }
2116 _ => vec![],
2117 }
2118 });
2119
2120 let ir = crate::parse(xml).expect("schema should parse");
2121 let schema = crate::Schema::from_ir(ir);
2122 let modules = crate::Generator::new(config).generate(&schema)?;
2123 let src = modules.modules().next().expect("one module").source.clone();
2124
2125 assert!(
2127 src.contains("impl serde::Serialize for EventCode"),
2128 "missing Serialize for enum"
2129 );
2130 assert!(src.contains("\"Ok\""), "missing Ok label");
2131 assert!(src.contains("\"Error\""), "missing Error label");
2132 assert!(
2133 src.contains("impl<'de> serde::Deserialize<'de> for EventCode"),
2134 "missing Deserialize for enum"
2135 );
2136 assert!(
2137 src.contains("unknown_variant"),
2138 "missing error handling in Deserialize"
2139 );
2140
2141 assert!(
2143 src.contains("impl serde::Serialize for OptionalFields"),
2144 "missing Serialize for set"
2145 );
2146 assert!(src.contains("\"hasPrice\""), "missing hasPrice label");
2147 assert!(
2148 src.contains("impl<'de> serde::Deserialize<'de> for OptionalFields"),
2149 "missing Deserialize for set"
2150 );
2151
2152 Ok(())
2153 }
2154
2155 #[test]
2159 fn with_bool_domain_type_works_with_generate_multi() -> Result<(), Box<dyn std::error::Error>> {
2160 let xml_a = r#"<?xml version="1.0"?>
2161 <messageSchema package="a" id="1" version="0" byteOrder="littleEndian">
2162 <types>
2163 <composite name="messageHeader">
2164 <type name="blockLength" primitiveType="uint16"/>
2165 <type name="templateId" primitiveType="uint16"/>
2166 <type name="schemaId" primitiveType="uint16"/>
2167 <type name="version" primitiveType="uint16"/>
2168 </composite>
2169 <enum name="BooleanType" encodingType="uint8">
2170 <validValue name="F">0</validValue>
2171 <validValue name="T">1</validValue>
2172 </enum>
2173 </types>
2174 <message name="MsgA" id="1" blockLength="1">
2175 <field name="flag" id="1" type="BooleanType" offset="0"/>
2176 </message>
2177 </messageSchema>"#;
2178 let xml_b = r#"<?xml version="1.0"?>
2179 <messageSchema package="b" id="2" version="0" byteOrder="littleEndian">
2180 <types>
2181 <composite name="messageHeader">
2182 <type name="blockLength" primitiveType="uint16"/>
2183 <type name="templateId" primitiveType="uint16"/>
2184 <type name="schemaId" primitiveType="uint16"/>
2185 <type name="version" primitiveType="uint16"/>
2186 </composite>
2187 <enum name="BooleanType" encodingType="uint8">
2188 <validValue name="F">0</validValue>
2189 <validValue name="T">1</validValue>
2190 </enum>
2191 </types>
2192 <message name="MsgB" id="1" blockLength="1">
2193 <field name="enabled" id="1" type="BooleanType" offset="0"/>
2194 </message>
2195 </messageSchema>"#;
2196
2197 let schema_a = Schema::from_ir(crate::parse(xml_a)?);
2198 let schema_b = Schema::from_ir(crate::parse(xml_b)?);
2199 let mut generator = Generator::new(
2200 crate::GenerationConfig::new("common_types")
2201 .with_shared_module("common_types")
2202 .with_bool_domain_type(true),
2203 );
2204 let modules =
2205 generator.generate_multi(&[(&schema_a, "common_types"), (&schema_b, "consumer")])?;
2206 let collected: Vec<_> = modules.modules().collect();
2207 assert_eq!(collected.len(), 2);
2208
2209 let consumer_src = &collected[1].source;
2214 assert!(
2215 consumer_src.contains("fn try_enabled_bool"),
2216 "with_bool_domain_type must produce bool getter in multi-schema; got:\n{consumer_src}",
2217 );
2218 Ok(())
2219 }
2220
2221 fn ping_schema_xml() -> &'static str {
2222 r#"<?xml version="1.0"?>
2223 <messageSchema package="ex" id="1" version="0" byteOrder="littleEndian">
2224 <types>
2225 <composite name="messageHeader">
2226 <type name="blockLength" primitiveType="uint16"/>
2227 <type name="templateId" primitiveType="uint16"/>
2228 <type name="schemaId" primitiveType="uint16"/>
2229 <type name="version" primitiveType="uint16"/>
2230 </composite>
2231 </types>
2232 <message name="Ping" id="1" blockLength="4">
2233 <field name="seq" id="1" type="uint32" offset="0"/>
2234 </message>
2235 </messageSchema>"#
2236 }
2237
2238 fn generate_ping(config: GenerationConfig) -> Result<String, Box<dyn std::error::Error>> {
2239 let schema = Schema::from_ir(crate::parse(ping_schema_xml())?);
2240 let src = Generator::new(config)
2241 .generate(&schema)?
2242 .modules()
2243 .next()
2244 .ok_or("no module")?
2245 .source
2246 .clone();
2247 Ok(src)
2248 }
2249
2250 #[test]
2252 fn size_knobs_omit_display_meta_and_dispatch() -> Result<(), Box<dyn std::error::Error>> {
2253 let full = generate_ping(GenerationConfig::new("ping"))?;
2254 assert!(
2255 full.contains("core::fmt::Display for PingDecoder"),
2256 "default must emit Display for PingDecoder; got marker search fail in {} chars",
2257 full.len()
2258 );
2259 assert!(
2260 full.contains("core::fmt::Debug for PingDecoder"),
2261 "default must emit Debug for PingDecoder"
2262 );
2263 assert!(
2264 full.contains("SEQ_ENCODING_OFFSET"),
2265 "default must emit field ENCODING_OFFSET constants"
2266 );
2267 assert!(
2268 full.contains("seq_meta_attribute"),
2269 "default must emit field meta_attribute fn"
2270 );
2271 assert!(
2272 full.contains("ping_field_meta"),
2273 "default must emit per-message field_meta module"
2274 );
2275 assert!(
2276 full.contains("enum AnyMessage"),
2277 "default must emit AnyMessage dispatch"
2278 );
2279
2280 let lean = generate_ping(
2281 GenerationConfig::new("ping")
2282 .with_display_debug(false)
2283 .with_meta_attributes(false)
2284 .with_dispatch(false),
2285 )?;
2286 assert!(
2287 !lean.contains("core::fmt::Display for PingDecoder")
2288 && !lean.contains("core::fmt::Debug for PingDecoder"),
2289 "with_display_debug(false) must omit Display/Debug"
2290 );
2291 assert!(
2292 !lean.contains("SEQ_ENCODING_OFFSET") && !lean.contains("seq_meta_attribute"),
2293 "with_meta_attributes(false) must omit field meta constants"
2294 );
2295 assert!(
2296 !lean.contains("ping_field_meta"),
2297 "with_meta_attributes(false) must omit field_meta module"
2298 );
2299 assert!(
2300 !lean.contains("enum AnyMessage") && !lean.contains("struct FrameCursor"),
2301 "with_dispatch(false) must omit AnyMessage/FrameCursor"
2302 );
2303 assert!(lean.contains("struct PingDecoder"));
2305 assert!(lean.contains("struct PingEncoder"));
2306 Ok(())
2307 }
2308}