1use std::collections::HashSet;
24use std::env;
25use std::fs;
26use std::path::{Path, PathBuf};
27
28use crate::codegen::{GenerateError, GeneratedModuleSet, Generator};
29use crate::config::GenerationConfig;
30use crate::schema::Schema;
31use crate::xml::{ParseError, parse, parse_file_with_deps, parse_file_with_shared_deps};
32
33#[derive(Debug, thiserror::Error, miette::Diagnostic)]
40pub enum BuildError {
41 #[error(transparent)]
43 #[diagnostic(transparent)]
44 Parse(#[from] ParseError),
45 #[error(transparent)]
47 Generate(#[from] GenerateError),
48 #[error("OUT_DIR is not set (run from a Cargo build.rs script)")]
50 MissingOutDir,
51 #[error("I/O error: {0}")]
53 Io(#[from] std::io::Error),
54 #[error("schema generated no modules")]
56 Empty,
57}
58
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64pub struct SchemaFile<'a> {
65 pub path: &'a Path,
67 pub module_name: &'a str,
69}
70
71impl<'a> SchemaFile<'a> {
72 #[must_use]
74 pub fn new(path: &'a Path, module_name: &'a str) -> Self {
75 Self { path, module_name }
76 }
77}
78
79pub fn generate_to_out_dir(
104 schema_path: impl AsRef<Path>,
105 config: GenerationConfig,
106) -> Result<GeneratedModuleSet, BuildError> {
107 generate_to_dir(schema_path, config, &out_dir()?)
108}
109
110pub fn generate_to_dir(
148 schema_path: impl AsRef<Path>,
149 config: GenerationConfig,
150 out_dir: impl AsRef<Path>,
151) -> Result<GeneratedModuleSet, BuildError> {
152 let schema_path = schema_path.as_ref();
153 let out_dir = out_dir.as_ref();
154 fs::create_dir_all(out_dir)?;
155 let parsed = parse_file_with_deps(schema_path)?;
156 let modules = write_generated(Schema::from_ir(parsed.ir), config, out_dir)?;
157 for watched in schema_watch_paths(schema_path, &parsed.dependencies) {
158 println!("cargo::rerun-if-changed={}", watched.display());
159 }
160 let is_cargo_out = env::var_os("OUT_DIR")
163 .map(|od| out_dir.starts_with(Path::new(&od)))
164 .unwrap_or(false);
165 if !is_cargo_out {
166 println!(
167 "cargo::warning=ergo-sbe wrote {} module(s) under {} (open for go-to-definition)",
168 modules.modules().len(),
169 out_dir.display()
170 );
171 }
172 Ok(modules)
173}
174
175pub fn generate_str_to_out_dir(
186 schema_xml: &str,
187 config: GenerationConfig,
188) -> Result<GeneratedModuleSet, BuildError> {
189 generate_str_to_dir(schema_xml, config, &out_dir()?)
190}
191
192pub fn generate_str_to_dir(
201 schema_xml: &str,
202 config: GenerationConfig,
203 out_dir: &Path,
204) -> Result<GeneratedModuleSet, BuildError> {
205 let ir = parse(schema_xml)?;
206 write_generated(Schema::from_ir(ir), config, out_dir)
207}
208
209pub fn generate_multi_to_out_dir(
220 shared: SchemaFile<'_>,
221 consumers: &[SchemaFile<'_>],
222 config: GenerationConfig,
223) -> Result<GeneratedModuleSet, BuildError> {
224 generate_multi_to_dir(shared, consumers, config, &out_dir()?)
225}
226
227pub fn generate_multi_to_dir(
250 shared: SchemaFile<'_>,
251 consumers: &[SchemaFile<'_>],
252 mut config: GenerationConfig,
253 out_dir: impl AsRef<Path>,
254) -> Result<GeneratedModuleSet, BuildError> {
255 if let Some(ref name) = config.shared_module {
256 if name != shared.module_name {
257 return Err(BuildError::Generate(GenerateError::InvalidConfiguration {
258 option: "shared_module".into(),
259 value: name.clone(),
260 reason: format!("must match shared.module_name {:?}", shared.module_name),
261 }));
262 }
263 } else {
264 config = config.with_shared_module(shared.module_name);
265 }
266
267 let shared_parsed = parse_file_with_deps(shared.path)?;
268 let shared_ir = shared_parsed.ir;
269 let mut watch = schema_watch_paths(shared.path, &shared_parsed.dependencies);
270 let mut consumer_irs = Vec::with_capacity(consumers.len());
271 for consumer in consumers {
272 let parsed = parse_file_with_shared_deps(consumer.path, &shared_ir)?;
273 watch.extend(schema_watch_paths(consumer.path, &parsed.dependencies));
274 consumer_irs.push((
275 Schema::from_ir(ir_with_shared_type_tokens(parsed.ir, &shared_ir)),
276 consumer.module_name,
277 ));
278 }
279 let shared_schema = Schema::from_ir(shared_ir);
280
281 let mut schemas: Vec<(&Schema, &str)> = Vec::with_capacity(1 + consumer_irs.len());
282 schemas.push((&shared_schema, shared.module_name));
283 for (schema, name) in &consumer_irs {
284 schemas.push((schema, name));
285 }
286
287 let modules = Generator::new(config).generate_multi(&schemas)?;
288 write_module_set(&modules, out_dir.as_ref())?;
289 let mut seen = HashSet::new();
290 for path in watch {
291 let key = path.canonicalize().unwrap_or_else(|_| path.clone());
292 if seen.insert(key) {
293 println!("cargo::rerun-if-changed={}", path.display());
294 }
295 }
296 Ok(modules)
297}
298
299pub fn out_dir() -> Result<PathBuf, BuildError> {
305 env::var_os("OUT_DIR")
306 .map(PathBuf::from)
307 .ok_or(BuildError::MissingOutDir)
308}
309
310pub(crate) fn schema_watch_paths(root: &Path, dependencies: &[PathBuf]) -> Vec<PathBuf> {
313 let mut seen = HashSet::new();
314 let mut out = Vec::new();
315 let mut push = |p: &Path| {
316 let key = p.canonicalize().unwrap_or_else(|_| p.to_path_buf());
317 if seen.insert(key) {
318 out.push(p.to_path_buf());
319 }
320 };
321 push(root);
322 let mut rest: Vec<&PathBuf> = dependencies.iter().collect();
323 rest.sort();
324 for path in rest {
325 push(path);
326 }
327 out
328}
329
330fn ir_with_shared_type_tokens(mut consumer: crate::Ir, shared: &crate::Ir) -> crate::Ir {
333 let mut extra = Vec::new();
334 let mut i = 0;
335 while i < shared.tokens.len() {
336 if shared.tokens[i].signal == crate::Signal::BeginMessage {
337 while i < shared.tokens.len() && shared.tokens[i].signal != crate::Signal::EndMessage {
338 i += 1;
339 }
340 i += 1;
341 continue;
342 }
343 extra.push(shared.tokens[i].clone());
344 i += 1;
345 }
346 extra.append(&mut consumer.tokens);
347 consumer.tokens = extra;
348 consumer
349}
350
351fn write_generated(
352 schema: Schema,
353 config: GenerationConfig,
354 out: &Path,
355) -> Result<GeneratedModuleSet, BuildError> {
356 let modules = Generator::new(config).generate(&schema)?;
357 write_module_set(&modules, out)?;
358 Ok(modules)
359}
360
361fn write_module_set(modules: &GeneratedModuleSet, out: &Path) -> Result<(), BuildError> {
362 if modules.modules().len() == 0 {
363 return Err(BuildError::Empty);
364 }
365 fs::create_dir_all(out)?;
366 for m in modules.modules() {
367 let path_str = &m.path;
370 if path_str.contains('/') || path_str.contains('\\') || path_str.contains("..") {
371 return Err(BuildError::Generate(
372 crate::codegen::GenerateError::InvalidConfiguration {
373 option: "module_path".into(),
374 value: path_str.clone(),
375 reason: "module path must be a plain .rs basename — no path separators".into(),
376 },
377 ));
378 }
379 let dest = out.join(&m.path);
380 if let Some(parent) = dest.parent() {
381 fs::create_dir_all(parent)?;
382 }
383 fs::write(&dest, &m.source)?;
384 }
385 for w in modules.warnings() {
386 println!("cargo::warning={w}");
387 }
388 Ok(())
389}
390
391#[macro_export]
400macro_rules! include_sbe {
401 ($module:literal) => {
402 include!(concat!(env!("OUT_DIR"), "/", $module, ".rs"));
403 };
404 ($module:ident) => {
405 include!(concat!(env!("OUT_DIR"), "/", stringify!($module), ".rs"));
406 };
407}
408
409#[macro_export]
420macro_rules! sbe_mod {
421 ($name:ident) => {
422 mod $name {
423 #![allow(
424 dead_code,
425 unused_imports,
426 unused_variables,
427 unused_mut,
428 unused_assignments,
429 unused_must_use,
430 unused_comparisons,
431 non_camel_case_types,
432 non_snake_case,
433 unexpected_cfgs,
434 unused_unsafe,
435 clippy::all,
436 clippy::pedantic,
437 clippy::nursery,
438 clippy::unwrap_used,
439 clippy::expect_used,
440 clippy::panic
441 )]
442 include!(concat!(env!("OUT_DIR"), "/", stringify!($name), ".rs"));
443 }
444 };
445 ($vis:vis $name:ident) => {
446 $vis mod $name {
447 #![allow(
448 dead_code,
449 unused_imports,
450 unused_variables,
451 unused_mut,
452 unused_assignments,
453 unused_must_use,
454 unused_comparisons,
455 unused_unsafe,
456 non_camel_case_types,
457 non_snake_case,
458 unexpected_cfgs,
459 clippy::all,
460 clippy::pedantic,
461 clippy::nursery,
462 clippy::unwrap_used,
463 clippy::expect_used,
464 clippy::panic
465 )]
466 include!(concat!(env!("OUT_DIR"), "/", stringify!($name), ".rs"));
467 }
468 };
469 ($name:ident = $file:literal) => {
470 mod $name {
471 #![allow(
472 dead_code,
473 unused_imports,
474 unused_variables,
475 unused_mut,
476 unused_assignments,
477 unused_must_use,
478 unused_comparisons,
479 non_camel_case_types,
480 non_snake_case,
481 unexpected_cfgs,
482 clippy::all
483 )]
484 include!(concat!(env!("OUT_DIR"), "/", $file, ".rs"));
485 }
486 };
487 ($vis:vis $name:ident = $file:literal) => {
488 $vis mod $name {
489 #![allow(
490 dead_code,
491 unused_imports,
492 unused_variables,
493 unused_mut,
494 unused_assignments,
495 unused_must_use,
496 unused_comparisons,
497 non_camel_case_types,
498 non_snake_case,
499 unexpected_cfgs,
500 clippy::all
501 )]
502 include!(concat!(env!("OUT_DIR"), "/", $file, ".rs"));
503 }
504 };
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use std::sync::atomic::{AtomicU64, Ordering};
511
512 fn minimal_schema() -> &'static str {
513 r#"<?xml version="1.0"?>
514 <messageSchema package="t" id="1" version="0" byteOrder="littleEndian">
515 <types>
516 <composite name="messageHeader">
517 <type name="blockLength" primitiveType="uint16"/>
518 <type name="templateId" primitiveType="uint16"/>
519 <type name="schemaId" primitiveType="uint16"/>
520 <type name="version" primitiveType="uint16"/>
521 </composite>
522 </types>
523 <message name="Ping" id="1">
524 <field name="seq" id="1" type="uint32" offset="0"/>
525 </message>
526 </messageSchema>"#
527 }
528
529 #[test]
534 fn miette_is_reexported_for_build_rs_return_type() -> crate::miette::Result<()> {
535 fn build_rs_main() -> crate::miette::Result<()> {
536 Ok(())
537 }
538 build_rs_main()
539 }
540
541 #[test]
547 fn out_dir_helpers_report_missing_out_dir_outside_a_build_script() {
548 assert!(
549 env::var_os("OUT_DIR").is_none(),
550 "ergo-sbe has no build script; OUT_DIR must be unset for this test"
551 );
552 assert!(matches!(out_dir(), Err(BuildError::MissingOutDir)));
553 assert!(matches!(
554 generate_str_to_out_dir(minimal_schema(), GenerationConfig::new("ping")),
555 Err(BuildError::MissingOutDir)
556 ));
557 assert!(matches!(
558 generate_to_out_dir("schemas/does-not-matter.xml", GenerationConfig::new("ping")),
559 Err(BuildError::MissingOutDir)
560 ));
561 }
562
563 #[test]
564 fn generate_str_to_dir_writes_module() -> Result<(), Box<dyn std::error::Error>> {
565 let dir = tempfile_dir()?;
566 let set = generate_str_to_dir(minimal_schema(), GenerationConfig::new("ping"), &dir)?;
567 assert_eq!(set.modules().len(), 1);
568 let path = dir.join("ping.rs");
569 assert!(path.is_file(), "expected {}", path.display());
570 let src = fs::read_to_string(&path)?;
571 assert!(src.contains("PingEncoder"), "{src}");
572 assert!(src.contains("PingDecoder"), "{src}");
573 let _ = fs::remove_dir_all(&dir);
574 Ok(())
575 }
576
577 #[test]
578 fn generate_to_dir_reads_schema_file() -> Result<(), Box<dyn std::error::Error>> {
579 let dir = tempfile_dir()?;
580 let schema_path = dir.join("messages.xml");
581 fs::write(&schema_path, minimal_schema())?;
582
583 let explicit = dir.join("explicit");
584 let set = generate_to_dir(&schema_path, GenerationConfig::new("from_file"), &explicit)?;
585 assert_eq!(set.modules().len(), 1);
586 assert!(explicit.join("from_file.rs").is_file());
587
588 fs::remove_dir_all(&dir)?;
589 Ok(())
590 }
591
592 #[test]
593 fn schema_watch_paths_are_root_then_sorted_unique_includes()
594 -> Result<(), Box<dyn std::error::Error>> {
595 let dir = tempfile_dir()?;
596 let root = dir.join("root.xml");
597 let leaf = dir.join("leaf.xml");
598 let mid = dir.join("mid.xml");
599 fs::write(&root, "root")?;
600 fs::write(&leaf, "leaf")?;
601 fs::write(&mid, "mid")?;
602 let root_canon = root.canonicalize()?;
603 let leaf_canon = leaf.canonicalize()?;
604 let mid_canon = mid.canonicalize()?;
605 let watched = schema_watch_paths(
606 &root,
607 &[
608 mid_canon.clone(),
609 leaf_canon.clone(),
610 root_canon.clone(),
611 leaf_canon.clone(),
612 ],
613 );
614 assert_eq!(watched.first(), Some(&root));
615 assert_eq!(watched.len(), 3, "{watched:?}");
616 let rest: Vec<_> = watched.iter().skip(1).cloned().collect();
617 let mut expected_rest = vec![leaf_canon, mid_canon];
618 expected_rest.sort();
619 assert_eq!(rest, expected_rest);
620 fs::remove_dir_all(&dir)?;
621 Ok(())
622 }
623
624 #[test]
625 fn generate_to_dir_rebuilds_after_include_only_edit() -> Result<(), Box<dyn std::error::Error>>
626 {
627 let dir = tempfile_dir()?;
628 let types = dir.join("types.xml");
629 fs::write(
630 &types,
631 r#"<?xml version="1.0"?>
632<types>
633 <composite name="messageHeader">
634 <type name="blockLength" primitiveType="uint16"/>
635 <type name="templateId" primitiveType="uint16"/>
636 <type name="schemaId" primitiveType="uint16"/>
637 <type name="version" primitiveType="uint16"/>
638 </composite>
639 <type name="Seq" primitiveType="uint32"/>
640</types>
641"#,
642 )?;
643 let schema_path = dir.join("root.xml");
644 fs::write(
645 &schema_path,
646 r#"<?xml version="1.0"?>
647<messageSchema package="t" id="1" version="0" byteOrder="littleEndian">
648 <include href="types.xml"/>
649 <types/>
650 <message name="Ping" id="1">
651 <field name="seq" id="1" type="Seq"/>
652 </message>
653</messageSchema>
654"#,
655 )?;
656 let parsed = crate::xml::parse_file_with_deps(&schema_path)?;
657 let watched = schema_watch_paths(&schema_path, &parsed.dependencies);
658 assert!(
659 watched.iter().any(|p| p.file_name() == types.file_name()),
660 "include must be watched: {watched:?}"
661 );
662
663 let out = dir.join("out");
664 generate_to_dir(&schema_path, GenerationConfig::new("ping"), &out)?;
665 let first = fs::read_to_string(out.join("ping.rs"))?;
666 assert!(
667 first.contains("u32") || first.contains("uint32"),
668 "first generate must encode Seq as uint32:\n{first}"
669 );
670
671 fs::write(
672 &types,
673 r#"<?xml version="1.0"?>
674<types>
675 <composite name="messageHeader">
676 <type name="blockLength" primitiveType="uint16"/>
677 <type name="templateId" primitiveType="uint16"/>
678 <type name="schemaId" primitiveType="uint16"/>
679 <type name="version" primitiveType="uint16"/>
680 </composite>
681 <type name="Seq" primitiveType="uint64"/>
682</types>
683"#,
684 )?;
685 generate_to_dir(&schema_path, GenerationConfig::new("ping"), &out)?;
686 let second = fs::read_to_string(out.join("ping.rs"))?;
687 assert!(
688 second.contains("u64") || second.contains("uint64"),
689 "include-only edit must regenerate Seq as uint64:\n{second}"
690 );
691 assert_ne!(first, second, "generated source must change");
692 fs::remove_dir_all(&dir)?;
693 Ok(())
694 }
695
696 fn write_multi_schemas(dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
697 fs::write(
698 dir.join("common-types.xml"),
699 r#"<?xml version="1.0"?>
700<messageSchema package="common" id="0" version="1" byteOrder="littleEndian">
701 <types>
702 <composite name="messageHeader">
703 <type name="blockLength" primitiveType="uint16"/>
704 <type name="templateId" primitiveType="uint16"/>
705 <type name="schemaId" primitiveType="uint16"/>
706 <type name="version" primitiveType="uint16"/>
707 </composite>
708 <composite name="Price">
709 <type name="mantissa" primitiveType="int64"/>
710 <type name="exponent" primitiveType="int8"/>
711 </composite>
712 </types>
713</messageSchema>
714"#,
715 )?;
716 fs::write(
717 dir.join("orders.xml"),
718 r#"<?xml version="1.0"?>
719<messageSchema package="orders" id="1" version="1" byteOrder="littleEndian">
720 <message name="NewOrder" id="1">
721 <field name="price" id="1" type="Price"/>
722 </message>
723</messageSchema>
724"#,
725 )?;
726 Ok(())
727 }
728
729 #[test]
730 fn generate_multi_to_dir_uses_supplied_module_names() -> Result<(), Box<dyn std::error::Error>>
731 {
732 let dir = tempfile_dir()?;
733 write_multi_schemas(&dir)?;
734 let out = dir.join("out");
735 let set = generate_multi_to_dir(
736 SchemaFile::new(&dir.join("common-types.xml"), "common_types"),
737 &[SchemaFile::new(&dir.join("orders.xml"), "orders")],
738 GenerationConfig::new("common_types"),
739 &out,
740 )?;
741 let names: Vec<_> = set.modules().map(|m| m.path.as_str()).collect();
742 assert_eq!(names, ["common_types.rs", "orders.rs"]);
743 assert!(out.join("common_types.rs").is_file());
744 assert!(out.join("orders.rs").is_file());
745 let orders = fs::read_to_string(out.join("orders.rs"))?;
746 assert!(
747 orders.contains("price") || orders.contains("NewOrder"),
748 "shared Price must resolve in the consumer"
749 );
750 fs::remove_dir_all(&dir)?;
751 Ok(())
752 }
753
754 #[test]
755 fn generate_multi_rejects_mismatched_shared_module() -> Result<(), Box<dyn std::error::Error>> {
756 let dir = tempfile_dir()?;
757 write_multi_schemas(&dir)?;
758 let err = generate_multi_to_dir(
759 SchemaFile::new(&dir.join("common-types.xml"), "common_types"),
760 &[SchemaFile::new(&dir.join("orders.xml"), "orders")],
761 GenerationConfig::new("common_types").with_shared_module("other"),
762 dir.join("out"),
763 )
764 .unwrap_err();
765 let msg = err.to_string();
766 assert!(
767 msg.contains("shared_module") || msg.contains("other"),
768 "{msg}"
769 );
770 assert!(!dir.join("out/common_types.rs").exists());
771 fs::remove_dir_all(&dir)?;
772 Ok(())
773 }
774
775 #[test]
776 fn generate_multi_rejects_duplicate_module_names() -> Result<(), Box<dyn std::error::Error>> {
777 let dir = tempfile_dir()?;
778 write_multi_schemas(&dir)?;
779 let err = generate_multi_to_dir(
780 SchemaFile::new(&dir.join("common-types.xml"), "dup"),
781 &[SchemaFile::new(&dir.join("orders.xml"), "dup")],
782 GenerationConfig::new("dup"),
783 dir.join("out"),
784 )
785 .unwrap_err();
786 let msg = err.to_string();
787 assert!(msg.contains("duplicate") || msg.contains("dup"), "{msg}");
788 assert!(!dir.join("out/dup.rs").exists());
789 fs::remove_dir_all(&dir)?;
790 Ok(())
791 }
792
793 #[test]
794 fn generate_multi_late_consumer_failure_writes_nothing()
795 -> Result<(), Box<dyn std::error::Error>> {
796 let dir = tempfile_dir()?;
797 write_multi_schemas(&dir)?;
798 fs::write(
799 dir.join("bad.xml"),
800 r#"<?xml version="1.0"?>
801<messageSchema package="bad" id="2" version="1" byteOrder="littleEndian">
802 <message name="Bad" id="1">
803 <field name="x" id="1" type="NotAType"/>
804 </message>
805</messageSchema>
806"#,
807 )?;
808 let out = dir.join("out");
809 let err = generate_multi_to_dir(
810 SchemaFile::new(&dir.join("common-types.xml"), "common_types"),
811 &[
812 SchemaFile::new(&dir.join("orders.xml"), "orders"),
813 SchemaFile::new(&dir.join("bad.xml"), "bad"),
814 ],
815 GenerationConfig::new("common_types"),
816 &out,
817 )
818 .unwrap_err();
819 assert!(matches!(err, BuildError::Parse(_)), "{err:?}");
820 assert!(
821 !out.join("common_types.rs").exists() && !out.join("orders.rs").exists(),
822 "late consumer failure must not write earlier modules"
823 );
824 fs::remove_dir_all(&dir)?;
825 Ok(())
826 }
827
828 #[test]
829 fn generate_multi_watches_transitive_includes() -> Result<(), Box<dyn std::error::Error>> {
830 let dir = tempfile_dir()?;
831 let nested = dir.join("nested");
832 fs::create_dir_all(&nested)?;
833 fs::write(
834 dir.join("leaf.xml"),
835 r#"<?xml version="1.0"?>
836<types>
837 <composite name="Price">
838 <type name="mantissa" primitiveType="int64"/>
839 <type name="exponent" primitiveType="int8"/>
840 </composite>
841</types>
842"#,
843 )?;
844 fs::write(
845 nested.join("mid.xml"),
846 r#"<?xml version="1.0"?>
847<messageSchema package="mid" id="9" version="0">
848 <include href="../leaf.xml"/>
849 <types/>
850</messageSchema>
851"#,
852 )?;
853 fs::write(
854 dir.join("common-types.xml"),
855 r#"<?xml version="1.0"?>
856<messageSchema package="common" id="0" version="1" byteOrder="littleEndian">
857 <include href="nested/mid.xml"/>
858 <types>
859 <composite name="messageHeader">
860 <type name="blockLength" primitiveType="uint16"/>
861 <type name="templateId" primitiveType="uint16"/>
862 <type name="schemaId" primitiveType="uint16"/>
863 <type name="version" primitiveType="uint16"/>
864 </composite>
865 </types>
866</messageSchema>
867"#,
868 )?;
869 fs::write(
870 dir.join("orders.xml"),
871 r#"<?xml version="1.0"?>
872<messageSchema package="orders" id="1" version="1" byteOrder="littleEndian">
873 <message name="NewOrder" id="1">
874 <field name="price" id="1" type="Price"/>
875 </message>
876</messageSchema>
877"#,
878 )?;
879 let parsed = crate::xml::parse_file_with_deps(dir.join("common-types.xml"))?;
880 let names: Vec<String> = parsed
881 .dependencies
882 .iter()
883 .map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
884 .collect();
885 assert!(names.contains(&"leaf.xml".into()), "{names:?}");
886 let out = dir.join("out");
887 generate_multi_to_dir(
888 SchemaFile::new(&dir.join("common-types.xml"), "common_types"),
889 &[SchemaFile::new(&dir.join("orders.xml"), "orders")],
890 GenerationConfig::new("common_types"),
891 &out,
892 )?;
893 assert!(out.join("orders.rs").is_file());
894 fs::remove_dir_all(&dir)?;
895 Ok(())
896 }
897
898 #[test]
905 fn build_error_parse_variant_renders_source_snippet_via_miette()
906 -> Result<(), Box<dyn std::error::Error>> {
907 let bad_xml = r#"<messageSchema package="x" id="1" version="0">
908 <types><composite name="messageHeader"><type name="blockLength" primitiveType="uint16"/><type name="templateId" primitiveType="uint16"/><type name="schemaId" primitiveType="uint16"/><type name="version" primitiveType="uint16"/></composite></types>
909 <message name="M" id="1"><field name="f" id="1" type="bogus"/></message>
910</messageSchema>"#;
911
912 let dir = tempfile_dir()?;
913 let err = generate_str_to_dir(bad_xml, GenerationConfig::new("bad"), &dir).unwrap_err();
914 let _ = fs::remove_dir_all(&dir);
915
916 assert!(
917 matches!(err, BuildError::Parse(_)),
918 "expected BuildError::Parse, got {err:?}"
919 );
920
921 let mut rendered = String::new();
922 miette::GraphicalReportHandler::new_themed(miette::GraphicalTheme::unicode_nocolor())
923 .render_report(&mut rendered, &err)?;
924
925 assert!(rendered.contains("bogus"), "rendered:\n{rendered}");
926 assert!(
927 rendered.lines().count() > 1,
928 "expected a multi-line snippet through the transparent wrapper, got:\n{rendered}"
929 );
930
931 Ok(())
932 }
933
934 fn tempfile_dir() -> Result<PathBuf, Box<dyn std::error::Error>> {
935 static N: AtomicU64 = AtomicU64::new(0);
936 let dir = env::temp_dir().join(format!(
937 "ergo_sbe_build_test_{}_{}_{}",
938 std::process::id(),
939 N.fetch_add(1, Ordering::Relaxed),
940 std::time::SystemTime::now()
941 .duration_since(std::time::UNIX_EPOCH)?
942 .as_nanos()
943 ));
944 fs::create_dir_all(&dir)?;
945 Ok(dir)
946 }
947}