1#![cfg_attr(docsrs, feature(doc_cfg))]
2mod builders;
8#[cfg(feature = "fetch")]
9mod fetch;
10mod parsers;
11mod util;
12
13#[cfg(feature = "fetch")]
14pub use fetch::fetch_and_load_xml;
15
16use quick_xml::Reader;
17use quick_xml::events::{BytesStart, Event};
18use quick_xml::name::QName;
19use serde::{Deserialize, Serialize};
20use thiserror::Error;
21
22use parsers::{
23 parse_boolean, parse_category, parse_category_empty, parse_command, parse_command_empty,
24 parse_converter, parse_enum, parse_float, parse_int_converter, parse_integer, parse_register,
25 parse_string, parse_struct_reg, parse_swissknife,
26};
27use util::{attribute_value, skip_element};
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub enum EnumValueSrc {
32 Literal(i64),
34 FromNode(String),
36}
37
38#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
50pub struct PredicateRefs {
51 #[serde(
53 default,
54 skip_serializing_if = "Option::is_none",
55 rename = "pIsImplemented"
56 )]
57 pub p_is_implemented: Option<String>,
58 #[serde(
60 default,
61 skip_serializing_if = "Option::is_none",
62 rename = "pIsAvailable"
63 )]
64 pub p_is_available: Option<String>,
65 #[serde(default, skip_serializing_if = "Option::is_none", rename = "pIsLocked")]
67 pub p_is_locked: Option<String>,
68}
69
70impl PredicateRefs {
71 pub fn references(&self) -> impl Iterator<Item = &str> {
73 [
74 self.p_is_implemented.as_deref(),
75 self.p_is_available.as_deref(),
76 self.p_is_locked.as_deref(),
77 ]
78 .into_iter()
79 .flatten()
80 }
81
82 pub fn is_empty(&self) -> bool {
84 self.p_is_implemented.is_none()
85 && self.p_is_available.is_none()
86 && self.p_is_locked.is_none()
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub struct EnumEntryDecl {
93 pub name: String,
95 pub value: EnumValueSrc,
97 pub display_name: Option<String>,
99 #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
101 pub predicates: PredicateRefs,
102}
103
104#[derive(Debug, Error)]
105#[non_exhaustive]
106pub enum XmlError {
107 #[error("xml: {0}")]
108 Xml(String),
109 #[error("invalid descriptor: {0}")]
110 Invalid(String),
111 #[error("transport: {0}")]
112 Transport(String),
113 #[error("unsupported URL: {0}")]
114 Unsupported(String),
115}
116
117#[derive(
122 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
123)]
124#[non_exhaustive]
125pub enum Visibility {
126 #[default]
128 Beginner,
129 Expert,
131 Guru,
133 Invisible,
135}
136
137impl Visibility {
138 pub(crate) fn parse(s: &str) -> Option<Self> {
139 match s.trim() {
140 "Beginner" => Some(Self::Beginner),
141 "Expert" => Some(Self::Expert),
142 "Guru" => Some(Self::Guru),
143 "Invisible" => Some(Self::Invisible),
144 _ => None,
145 }
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
151#[non_exhaustive]
152pub enum Representation {
153 Linear,
154 Logarithmic,
155 Boolean,
156 PureNumber,
157 HexNumber,
158 IPV4Address,
160 MACAddress,
162}
163
164impl Representation {
165 pub(crate) fn parse(s: &str) -> Option<Self> {
166 match s.trim() {
167 "Linear" => Some(Self::Linear),
168 "Logarithmic" => Some(Self::Logarithmic),
169 "Boolean" => Some(Self::Boolean),
170 "PureNumber" => Some(Self::PureNumber),
171 "HexNumber" => Some(Self::HexNumber),
172 "IPV4Address" => Some(Self::IPV4Address),
173 "MACAddress" => Some(Self::MACAddress),
174 _ => None,
175 }
176 }
177}
178
179#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
181pub struct NodeMeta {
182 pub visibility: Visibility,
184 pub description: Option<String>,
186 pub tooltip: Option<String>,
188 pub display_name: Option<String>,
190 pub representation: Option<Representation>,
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
196pub enum AccessMode {
197 RO,
199 WO,
201 RW,
203}
204
205impl AccessMode {
206 pub(crate) fn parse(value: &str) -> Result<Self, XmlError> {
215 let trimmed = value.trim();
216 match trimmed.to_ascii_uppercase().as_str() {
217 "RO" | "R" => Ok(AccessMode::RO),
218 "WO" | "W" => Ok(AccessMode::WO),
219 "RW" | "WR" => Ok(AccessMode::RW),
220 other => {
221 tracing::warn!(
222 access_mode = other,
223 "unknown <AccessMode> value, assuming RW"
224 );
225 Ok(AccessMode::RW)
226 }
227 }
228 }
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
233pub enum Addressing {
234 Sum {
251 terms: Vec<AddressTerm>,
253 len: u32,
255 },
256 BySelector {
261 selector: String,
263 map: Vec<(String, (u64, u32))>,
265 },
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
270pub enum AddressTerm {
271 Fixed(u64),
273 Node(String),
275 Index {
277 node: String,
279 offset: IndexOffset,
281 },
282}
283
284#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
286pub enum IndexOffset {
287 Fixed(u64),
289 Node(String),
291 Length,
293}
294
295impl Addressing {
296 pub fn fixed(address: u64, len: u32) -> Self {
298 Addressing::Sum {
299 terms: vec![AddressTerm::Fixed(address)],
300 len,
301 }
302 }
303
304 pub fn byte_len(&self) -> Option<u32> {
306 match self {
307 Addressing::Sum { len, .. } => Some(*len),
308 Addressing::BySelector { .. } => None,
309 }
310 }
311
312 pub fn referenced_nodes(&self) -> Vec<&str> {
317 match self {
318 Addressing::Sum { terms, .. } => terms
319 .iter()
320 .flat_map(|term| match term {
321 AddressTerm::Fixed(_) => Vec::new(),
322 AddressTerm::Node(node) => vec![node.as_str()],
323 AddressTerm::Index { node, offset } => match offset {
324 IndexOffset::Node(stride) => vec![node.as_str(), stride.as_str()],
325 _ => vec![node.as_str()],
326 },
327 })
328 .collect(),
329 Addressing::BySelector { selector, .. } => vec![selector.as_str()],
330 }
331 }
332}
333
334#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
342pub enum Sign {
343 #[default]
345 Unsigned,
346 Signed,
348}
349
350impl Sign {
351 pub(crate) fn parse(tag: &str) -> Option<Self> {
352 match tag.trim().to_ascii_lowercase().as_str() {
353 "signed" => Some(Sign::Signed),
354 "unsigned" => Some(Sign::Unsigned),
355 _ => None,
356 }
357 }
358
359 pub fn is_signed(self) -> bool {
361 matches!(self, Sign::Signed)
362 }
363}
364
365#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
367pub enum ByteOrder {
368 Little,
370 Big,
372}
373
374impl ByteOrder {
375 pub(crate) fn parse(tag: &str) -> Option<Self> {
376 match tag.trim().to_ascii_lowercase().as_str() {
377 "littleendian" => Some(ByteOrder::Little),
378 "bigendian" => Some(ByteOrder::Big),
379 _ => None,
380 }
381 }
382}
383
384fn default_big_endian() -> ByteOrder {
385 ByteOrder::Big
386}
387
388#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
390pub struct BitField {
391 pub bit_offset: u16,
393 pub bit_length: u16,
395 pub byte_order: ByteOrder,
397}
398
399#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
413pub enum FloatEncoding {
414 Ieee754,
416 #[default]
419 ScaledInteger,
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
424pub enum SkOutput {
425 Integer,
428 #[default]
431 Float,
432}
433
434impl SkOutput {
435 pub(crate) fn parse(tag: &str) -> Option<Self> {
436 match tag.trim().to_ascii_lowercase().as_str() {
437 "integer" => Some(SkOutput::Integer),
438 "float" => Some(SkOutput::Float),
439 _ => None,
440 }
441 }
442}
443#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
458pub struct FormulaBindings {
459 #[serde(default, skip_serializing_if = "Vec::is_empty")]
461 pub constants: Vec<(String, String)>,
462 #[serde(default, skip_serializing_if = "Vec::is_empty")]
464 pub expressions: Vec<(String, String)>,
465}
466
467impl FormulaBindings {
468 pub fn is_empty(&self) -> bool {
470 self.constants.is_empty() && self.expressions.is_empty()
471 }
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct SwissKnifeDecl {
477 pub name: String,
479 pub meta: NodeMeta,
481 pub expr: String,
483 pub variables: Vec<(String, String)>,
485 pub output: SkOutput,
487 #[serde(default, skip_serializing_if = "FormulaBindings::is_empty")]
489 pub bindings: FormulaBindings,
490 #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
492 pub predicates: PredicateRefs,
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct ConverterDecl {
501 pub name: String,
503 pub meta: NodeMeta,
505 pub p_value: String,
507 pub formula_to: String,
511 pub formula_from: String,
514 pub variables_to: Vec<(String, String)>,
516 pub variables_from: Vec<(String, String)>,
518 #[serde(default, skip_serializing_if = "FormulaBindings::is_empty")]
520 pub bindings: FormulaBindings,
521 pub unit: Option<String>,
523 pub output: SkOutput,
525 #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
527 pub predicates: PredicateRefs,
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize)]
532pub struct IntConverterDecl {
533 pub name: String,
535 pub meta: NodeMeta,
537 pub p_value: String,
539 pub formula_to: String,
543 pub formula_from: String,
546 pub variables_to: Vec<(String, String)>,
548 pub variables_from: Vec<(String, String)>,
550 #[serde(default, skip_serializing_if = "FormulaBindings::is_empty")]
552 pub bindings: FormulaBindings,
553 pub unit: Option<String>,
555 #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
557 pub predicates: PredicateRefs,
558}
559
560#[derive(Debug, Clone, Serialize, Deserialize)]
562pub struct StringDecl {
563 pub name: String,
565 pub meta: NodeMeta,
567 pub addressing: Addressing,
569 pub access: AccessMode,
571 #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
573 pub predicates: PredicateRefs,
574}
575
576#[derive(Debug, Clone, Serialize, Deserialize)]
586pub struct RegisterDecl {
587 pub name: String,
589 pub meta: NodeMeta,
591 pub addressing: Addressing,
593 pub access: AccessMode,
595 #[serde(default, skip_serializing_if = "Option::is_none")]
599 pub port: Option<String>,
600 #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
602 pub predicates: PredicateRefs,
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize)]
607#[non_exhaustive]
608pub enum NodeDecl {
609 Integer {
611 name: String,
613 meta: NodeMeta,
615 addressing: Option<Addressing>,
617 len: u32,
619 access: AccessMode,
621 min: i64,
623 max: i64,
625 inc: Option<i64>,
627 unit: Option<String>,
629 bitfield: Option<BitField>,
631 #[serde(default)]
633 sign: Sign,
634 selectors: Vec<String>,
636 selected_if: Vec<(String, Vec<String>)>,
638 pvalue: Option<String>,
640 p_max: Option<String>,
642 p_min: Option<String>,
644 value: Option<i64>,
646 #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
648 predicates: PredicateRefs,
649 },
650 Float {
653 name: String,
654 meta: NodeMeta,
655 addressing: Option<Addressing>,
657 access: AccessMode,
658 min: f64,
659 max: f64,
660 unit: Option<String>,
661 scale: Option<(i64, i64)>,
663 offset: Option<f64>,
665 selectors: Vec<String>,
666 selected_if: Vec<(String, Vec<String>)>,
667 pvalue: Option<String>,
669 #[serde(default)]
673 encoding: FloatEncoding,
674 #[serde(default = "default_big_endian")]
677 byte_order: ByteOrder,
678 #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
680 predicates: PredicateRefs,
681 },
682 Enum {
684 name: String,
685 meta: NodeMeta,
686 addressing: Option<Addressing>,
688 access: AccessMode,
689 entries: Vec<EnumEntryDecl>,
690 default: Option<String>,
691 selectors: Vec<String>,
692 selected_if: Vec<(String, Vec<String>)>,
693 pvalue: Option<String>,
695 #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
697 predicates: PredicateRefs,
698 },
699 Boolean {
701 name: String,
702 meta: NodeMeta,
703 addressing: Option<Addressing>,
705 len: u32,
706 access: AccessMode,
707 bitfield: Option<BitField>,
708 selectors: Vec<String>,
709 selected_if: Vec<(String, Vec<String>)>,
710 pvalue: Option<String>,
712 on_value: Option<i64>,
714 off_value: Option<i64>,
716 #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
718 predicates: PredicateRefs,
719 },
720 Command {
722 name: String,
723 meta: NodeMeta,
724 address: Option<u64>,
726 len: u32,
727 pvalue: Option<String>,
729 command_value: Option<i64>,
731 #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
733 predicates: PredicateRefs,
734 },
735 Category {
737 name: String,
738 meta: NodeMeta,
739 children: Vec<String>,
740 #[serde(default, skip_serializing_if = "PredicateRefs::is_empty")]
742 predicates: PredicateRefs,
743 },
744 SwissKnife(SwissKnifeDecl),
746 Converter(ConverterDecl),
748 IntConverter(IntConverterDecl),
750 String(StringDecl),
752 Register(RegisterDecl),
754}
755
756impl NodeDecl {
757 pub fn name(&self) -> &str {
759 match self {
760 NodeDecl::Integer { name, .. }
761 | NodeDecl::Float { name, .. }
762 | NodeDecl::Enum { name, .. }
763 | NodeDecl::Boolean { name, .. }
764 | NodeDecl::Command { name, .. }
765 | NodeDecl::Category { name, .. } => name,
766 NodeDecl::SwissKnife(decl) => &decl.name,
767 NodeDecl::Converter(decl) => &decl.name,
768 NodeDecl::IntConverter(decl) => &decl.name,
769 NodeDecl::String(decl) => &decl.name,
770 NodeDecl::Register(decl) => &decl.name,
771 }
772 }
773
774 pub fn kind(&self) -> &'static str {
781 match self {
782 NodeDecl::Integer { .. } => "Integer",
783 NodeDecl::Float { .. } => "Float",
784 NodeDecl::Enum { .. } => "Enum",
785 NodeDecl::Boolean { .. } => "Boolean",
786 NodeDecl::Command { .. } => "Command",
787 NodeDecl::Category { .. } => "Category",
788 NodeDecl::SwissKnife(_) => "SwissKnife",
789 NodeDecl::Converter(_) => "Converter",
790 NodeDecl::IntConverter(_) => "IntConverter",
791 NodeDecl::String(_) => "String",
792 NodeDecl::Register(_) => "Register",
793 }
794 }
795}
796
797#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
804pub struct SkippedNode {
805 pub tag: String,
807 pub name: Option<String>,
809 pub error: String,
811}
812
813#[derive(Debug, Clone, Serialize, Deserialize)]
815pub struct XmlModel {
816 pub version: String,
818 pub nodes: Vec<NodeDecl>,
820 #[serde(default, skip_serializing_if = "Vec::is_empty")]
823 pub skipped: Vec<SkippedNode>,
824}
825
826#[derive(Debug, Clone, PartialEq, Eq)]
828pub struct MinimalXmlInfo {
829 pub schema_version: Option<String>,
830 pub top_level_features: Vec<String>,
831}
832
833pub fn parse_into_minimal_nodes(xml: &str) -> Result<MinimalXmlInfo, XmlError> {
835 let mut reader = Reader::from_str(xml);
836 reader.config_mut().trim_text(true);
837 reader.config_mut().allow_dangling_amp = true;
840 let mut buf = Vec::new();
841 let mut depth = 0usize;
842 let mut schema_version: Option<String> = None;
843 let mut top_level_features = Vec::new();
844
845 loop {
846 match reader.read_event_into(&mut buf) {
847 Ok(Event::Start(e)) => {
848 depth += 1;
849 handle_start(&e, depth, &mut schema_version, &mut top_level_features)?;
850 }
851 Ok(Event::Empty(e)) => {
852 depth += 1;
853 handle_start(&e, depth, &mut schema_version, &mut top_level_features)?;
854 depth = depth.saturating_sub(1);
855 }
856 Ok(Event::End(_)) => {
857 depth = depth.saturating_sub(1);
858 }
859 Ok(Event::Eof) => break,
860 Err(err) => return Err(XmlError::Xml(err.to_string())),
861 _ => {}
862 }
863 buf.clear();
864 }
865
866 Ok(MinimalXmlInfo {
867 schema_version,
868 top_level_features,
869 })
870}
871
872fn is_node_tag(tag: &[u8]) -> bool {
874 matches!(
875 tag,
876 b"Integer"
877 | b"IntReg"
878 | b"MaskedIntReg"
879 | b"IntSwissKnife"
880 | b"SwissKnife"
881 | b"Float"
882 | b"FloatReg"
883 | b"Enumeration"
884 | b"Boolean"
885 | b"Command"
886 | b"Category"
887 | b"Converter"
888 | b"IntConverter"
889 | b"Register"
890 | b"StringReg"
891 | b"String"
892 | b"StructReg"
893 )
894}
895
896fn unknown_node(tag: &[u8], start: &BytesStart<'_>) -> Result<Option<SkippedNode>, XmlError> {
906 let Some(name) = attribute_value(start, b"Name")? else {
907 return Ok(None);
908 };
909 let tag = String::from_utf8_lossy(tag).into_owned();
910 tracing::warn!(
911 tag = %tag,
912 node = %name,
913 "skipping node of an unsupported type"
914 );
915 Ok(Some(SkippedNode {
916 error: format!("unsupported node type <{tag}>"),
917 tag,
918 name: Some(name),
919 }))
920}
921
922fn parse_node(
925 reader: &mut Reader<&[u8]>,
926 start: BytesStart<'_>,
927) -> Result<Vec<NodeDecl>, XmlError> {
928 let tag = start.name().as_ref().to_vec();
929 let node = match tag.as_slice() {
930 b"Integer" | b"IntReg" | b"MaskedIntReg" => parse_integer(reader, start)?,
931 b"IntSwissKnife" | b"SwissKnife" => parse_swissknife(reader, start)?,
932 b"Float" | b"FloatReg" => parse_float(reader, start)?,
933 b"Enumeration" => parse_enum(reader, start)?,
934 b"Boolean" => parse_boolean(reader, start)?,
935 b"Command" => parse_command(reader, start)?,
936 b"Category" => parse_category(reader, start)?,
937 b"Converter" => parse_converter(reader, start)?,
938 b"IntConverter" => parse_int_converter(reader, start)?,
939 b"Register" => parse_register(reader, start)?,
940 b"StringReg" | b"String" => parse_string(reader, start)?,
941 b"StructReg" => return parse_struct_reg(reader, start),
942 other => {
943 return Err(XmlError::Invalid(format!(
944 "not a node element: {}",
945 String::from_utf8_lossy(other)
946 )));
947 }
948 };
949 Ok(vec![node])
950}
951
952fn parse_isolated_node(element: &str) -> Result<Vec<NodeDecl>, XmlError> {
958 let mut reader = Reader::from_str(element);
959 reader.config_mut().trim_text(true);
960 reader.config_mut().allow_dangling_amp = true;
961 let mut buf = Vec::new();
962 loop {
963 match reader.read_event_into(&mut buf) {
964 Ok(Event::Start(e)) => {
965 let start = e.into_owned();
966 return parse_node(&mut reader, start);
967 }
968 Ok(Event::Eof) => {
969 return Err(XmlError::Invalid("node element is empty".into()));
970 }
971 Err(err) => return Err(XmlError::Xml(err.to_string())),
972 _ => {}
974 }
975 }
976}
977
978pub fn parse(xml: &str) -> Result<XmlModel, XmlError> {
989 let mut reader = Reader::from_str(xml);
990 reader.config_mut().trim_text(true);
991 reader.config_mut().allow_dangling_amp = true;
994 let mut buf = Vec::new();
995 let mut version = String::from("0.0.0");
996 let mut nodes = Vec::new();
997 let mut skipped = Vec::new();
998
999 loop {
1000 let element_start = reader.buffer_position() as usize;
1003 match reader.read_event_into(&mut buf) {
1004 Ok(Event::Start(ref e)) => match e.name().as_ref() {
1005 b"RegisterDescription" => {
1006 version = schema_version_from(e)?;
1007 }
1008 b"Group" => {
1009 }
1012 b"Port" => {
1013 skip_element(&mut reader, e.name().as_ref())?;
1015 }
1016 tag if is_node_tag(tag) => {
1017 let tag = tag.to_vec();
1018 let name = attribute_value(e, b"Name")?;
1019 reader
1022 .read_to_end(QName(&tag))
1023 .map_err(|err| XmlError::Xml(err.to_string()))?;
1024 let element = &xml[element_start..reader.buffer_position() as usize];
1025 match parse_isolated_node(element) {
1026 Ok(parsed) => nodes.extend(parsed),
1027 Err(err) => {
1028 let tag = String::from_utf8_lossy(&tag).into_owned();
1029 tracing::warn!(
1030 tag = %tag,
1031 node = name.as_deref().unwrap_or("<unnamed>"),
1032 error = %err,
1033 "skipping unparsable node"
1034 );
1035 skipped.push(SkippedNode {
1036 tag,
1037 name,
1038 error: err.to_string(),
1039 });
1040 }
1041 }
1042 }
1043 tag => {
1044 let unknown = unknown_node(tag, e)?;
1045 skip_element(&mut reader, e.name().as_ref())?;
1046 if let Some(record) = unknown {
1047 skipped.push(record);
1048 }
1049 }
1050 },
1051 Ok(Event::Empty(ref e)) => match e.name().as_ref() {
1052 b"RegisterDescription" => {
1053 version = schema_version_from(e)?;
1054 }
1055 b"Command" => {
1056 let node = parse_command_empty(e)?;
1057 nodes.push(node);
1058 }
1059 b"Category" => {
1060 let node = parse_category_empty(e)?;
1061 nodes.push(node);
1062 }
1063 b"Port" => {}
1066 tag => {
1067 if let Some(record) = unknown_node(tag, e)? {
1068 skipped.push(record);
1069 }
1070 }
1071 },
1072 Ok(Event::Eof) => break,
1073 Err(err) => return Err(XmlError::Xml(err.to_string())),
1074 _ => {}
1075 }
1076 buf.clear();
1077 }
1078
1079 if !skipped.is_empty() {
1080 tracing::warn!(
1081 count = skipped.len(),
1082 total = nodes.len() + skipped.len(),
1083 "some GenApi nodes could not be parsed and were skipped"
1084 );
1085 }
1086
1087 Ok(XmlModel {
1088 version,
1089 nodes,
1090 skipped,
1091 })
1092}
1093
1094fn schema_version_from(event: &BytesStart<'_>) -> Result<String, XmlError> {
1095 let major = attribute_value(event, b"SchemaMajorVersion")?;
1096 let minor = attribute_value(event, b"SchemaMinorVersion")?;
1097 let sub = attribute_value(event, b"SchemaSubMinorVersion")?;
1098 let major = major.unwrap_or_else(|| "0".to_string());
1099 let minor = minor.unwrap_or_else(|| "0".to_string());
1100 let sub = sub.unwrap_or_else(|| "0".to_string());
1101 Ok(format!("{major}.{minor}.{sub}"))
1102}
1103
1104fn handle_start(
1105 event: &BytesStart<'_>,
1106 depth: usize,
1107 schema_version: &mut Option<String>,
1108 top_level: &mut Vec<String>,
1109) -> Result<(), XmlError> {
1110 if depth == 1 && schema_version.is_none() {
1111 *schema_version = extract_schema_version(event);
1112 } else if depth == 2 {
1113 if let Some(name) = attribute_value(event, b"Name")? {
1114 top_level.push(name);
1115 } else {
1116 top_level.push(String::from_utf8_lossy(event.name().as_ref()).to_string());
1117 }
1118 }
1119 Ok(())
1120}
1121
1122fn extract_schema_version(event: &BytesStart<'_>) -> Option<String> {
1123 let major = attribute_value(event, b"SchemaMajorVersion").ok().flatten();
1124 let minor = attribute_value(event, b"SchemaMinorVersion").ok().flatten();
1125 let sub = attribute_value(event, b"SchemaSubMinorVersion")
1126 .ok()
1127 .flatten();
1128 if major.is_none() && minor.is_none() && sub.is_none() {
1129 None
1130 } else {
1131 let major = major.unwrap_or_else(|| "0".to_string());
1132 let minor = minor.unwrap_or_else(|| "0".to_string());
1133 let sub = sub.unwrap_or_else(|| "0".to_string());
1134 Some(format!("{major}.{minor}.{sub}"))
1135 }
1136}
1137
1138#[cfg(test)]
1139mod tests {
1140 use super::*;
1141
1142 const FIXTURE: &str = r#"
1143 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="2" SchemaSubMinorVersion="3">
1144 <Category Name="Root">
1145 <pFeature>Gain</pFeature>
1146 <pFeature>GainSelector</pFeature>
1147 </Category>
1148 <Integer Name="Width">
1149 <Address>0x0000_0100</Address>
1150 <Length>4</Length>
1151 <AccessMode>RW</AccessMode>
1152 <Min>16</Min>
1153 <Max>4096</Max>
1154 <Inc>2</Inc>
1155 </Integer>
1156 <Float Name="ExposureTime">
1157 <Address>0x0000_0200</Address>
1158 <Length>4</Length>
1159 <AccessMode>RW</AccessMode>
1160 <Min>10.0</Min>
1161 <Max>200000.0</Max>
1162 <Scale>1/1000</Scale>
1163 <Offset>0.0</Offset>
1164 </Float>
1165 <Enumeration Name="GainSelector">
1166 <Address>0x0000_0300</Address>
1167 <Length>2</Length>
1168 <AccessMode>RW</AccessMode>
1169 <EnumEntry Name="AnalogAll" Value="0" />
1170 <EnumEntry Name="DigitalAll" Value="1" />
1171 </Enumeration>
1172 <Integer Name="Gain">
1173 <Address>0x0000_0304</Address>
1174 <Length>2</Length>
1175 <AccessMode>RW</AccessMode>
1176 <Min>0</Min>
1177 <Max>48</Max>
1178 <pSelected>GainSelector</pSelected>
1179 <Selected>AnalogAll</Selected>
1180 </Integer>
1181 <Boolean Name="GammaEnable">
1182 <Address>0x0000_0400</Address>
1183 <Length>1</Length>
1184 <AccessMode>RW</AccessMode>
1185 </Boolean>
1186 <Command Name="AcquisitionStart">
1187 <Address>0x0000_0500</Address>
1188 <Length>4</Length>
1189 </Command>
1190 </RegisterDescription>
1191 "#;
1192
1193 #[test]
1194 fn parse_minimal_xml() {
1195 let info = parse_into_minimal_nodes(FIXTURE).expect("parse xml");
1196 assert_eq!(info.schema_version.as_deref(), Some("1.2.3"));
1197 assert_eq!(info.top_level_features.len(), 7);
1198 assert_eq!(info.top_level_features[0], "Root");
1199 }
1200
1201 #[test]
1202 fn parse_fixture_model() {
1203 let model = parse(FIXTURE).expect("parse fixture");
1204 assert_eq!(model.version, "1.2.3");
1205 assert_eq!(model.nodes.len(), 7);
1206 match &model.nodes[0] {
1207 NodeDecl::Category { name, children, .. } => {
1208 assert_eq!(name, "Root");
1209 assert_eq!(
1210 children,
1211 &vec!["Gain".to_string(), "GainSelector".to_string()]
1212 );
1213 }
1214 other => panic!("unexpected node: {other:?}"),
1215 }
1216 match &model.nodes[1] {
1217 NodeDecl::Integer {
1218 name,
1219 min,
1220 max,
1221 inc,
1222 ..
1223 } => {
1224 assert_eq!(name, "Width");
1225 assert_eq!(*min, 16);
1226 assert_eq!(*max, 4096);
1227 assert_eq!(*inc, Some(2));
1228 }
1229 other => panic!("unexpected node: {other:?}"),
1230 }
1231 match &model.nodes[2] {
1232 NodeDecl::Float {
1233 name,
1234 scale,
1235 offset,
1236 ..
1237 } => {
1238 assert_eq!(name, "ExposureTime");
1239 assert_eq!(*scale, Some((1, 1000)));
1240 assert_eq!(*offset, Some(0.0));
1241 }
1242 other => panic!("unexpected node: {other:?}"),
1243 }
1244 match &model.nodes[3] {
1245 NodeDecl::Enum { name, entries, .. } => {
1246 assert_eq!(name, "GainSelector");
1247 assert_eq!(entries.len(), 2);
1248 assert!(matches!(entries[0].value, EnumValueSrc::Literal(0)));
1249 assert!(matches!(entries[1].value, EnumValueSrc::Literal(1)));
1250 }
1251 other => panic!("unexpected node: {other:?}"),
1252 }
1253 match &model.nodes[4] {
1254 NodeDecl::Integer {
1255 name, selected_if, ..
1256 } => {
1257 assert_eq!(name, "Gain");
1258 assert_eq!(selected_if.len(), 1);
1259 assert_eq!(selected_if[0].0, "GainSelector");
1260 assert_eq!(selected_if[0].1, vec!["AnalogAll".to_string()]);
1261 }
1262 other => panic!("unexpected node: {other:?}"),
1263 }
1264 }
1265
1266 #[test]
1267 fn parse_swissknife_node() {
1268 const XML: &str = r#"
1269 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1270 <Integer Name="GainRaw">
1271 <Address>0x3000</Address>
1272 <Length>4</Length>
1273 <AccessMode>RW</AccessMode>
1274 <Min>0</Min>
1275 <Max>1000</Max>
1276 </Integer>
1277 <Float Name="Offset">
1278 <Address>0x3008</Address>
1279 <Length>4</Length>
1280 <AccessMode>RW</AccessMode>
1281 <Min>-100.0</Min>
1282 <Max>100.0</Max>
1283 </Float>
1284 <SwissKnife Name="ComputedGain">
1285 <Expression>(GainRaw * 0.5) + Offset</Expression>
1286 <pVariable Name="GainRaw">GainRaw</pVariable>
1287 <pVariable Name="Offset">Offset</pVariable>
1288 <Output>Float</Output>
1289 </SwissKnife>
1290 </RegisterDescription>
1291 "#;
1292
1293 let model = parse(XML).expect("parse swissknife xml");
1294 assert_eq!(model.nodes.len(), 3);
1295 let swiss = model
1296 .nodes
1297 .iter()
1298 .find_map(|decl| match decl {
1299 NodeDecl::SwissKnife(node) => Some(node),
1300 _ => None,
1301 })
1302 .expect("swissknife present");
1303 assert_eq!(swiss.name, "ComputedGain");
1304 assert_eq!(swiss.expr, "(GainRaw * 0.5) + Offset");
1305 assert_eq!(swiss.output, SkOutput::Float);
1306 assert_eq!(swiss.variables.len(), 2);
1307 assert_eq!(
1308 swiss.variables[0],
1309 ("GainRaw".to_string(), "GainRaw".to_string())
1310 );
1311 assert_eq!(
1312 swiss.variables[1],
1313 ("Offset".to_string(), "Offset".to_string())
1314 );
1315 }
1316
1317 #[test]
1318 fn parse_int_swissknife_with_hex_and_ampersand() {
1319 const XML: &str = r#"
1321 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1322 <IntSwissKnife Name="PayloadSize">
1323 <pVariable Name="W">Width</pVariable>
1324 <pVariable Name="H">Height</pVariable>
1325 <pVariable Name="PF">PixelFormat</pVariable>
1326 <Formula>W * H * ((PF>>16)&0xFF) / 8</Formula>
1327 </IntSwissKnife>
1328 </RegisterDescription>
1329 "#;
1330
1331 let model = parse(XML).expect("parse intswissknife");
1332 assert_eq!(model.nodes.len(), 1);
1333 let swiss = model
1334 .nodes
1335 .iter()
1336 .find_map(|decl| match decl {
1337 NodeDecl::SwissKnife(node) => Some(node),
1338 _ => None,
1339 })
1340 .expect("swissknife present");
1341 assert_eq!(swiss.name, "PayloadSize");
1342 assert!(
1344 swiss.expr.contains('&'),
1345 "expression should contain decoded '&': {}",
1346 swiss.expr
1347 );
1348 assert!(
1349 swiss.expr.contains("0xFF"),
1350 "expression should contain hex literal: {}",
1351 swiss.expr
1352 );
1353 }
1354
1355 #[test]
1356 fn parse_enum_entry_with_pvalue() {
1357 const XML: &str = r#"
1358 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1359 <Enumeration Name="Mode">
1360 <Address>0x0000_4000</Address>
1361 <Length>4</Length>
1362 <AccessMode>RW</AccessMode>
1363 <EnumEntry Name="Fixed10">
1364 <Value>10</Value>
1365 </EnumEntry>
1366 <EnumEntry Name="DynFromReg">
1367 <pValue>RegModeVal</pValue>
1368 </EnumEntry>
1369 </Enumeration>
1370 <Integer Name="RegModeVal">
1371 <Address>0x0000_4100</Address>
1372 <Length>4</Length>
1373 <AccessMode>RW</AccessMode>
1374 <Min>0</Min>
1375 <Max>65535</Max>
1376 </Integer>
1377 </RegisterDescription>
1378 "#;
1379
1380 let model = parse(XML).expect("parse enum pvalue");
1381 assert_eq!(model.nodes.len(), 2);
1382 match &model.nodes[0] {
1383 NodeDecl::Enum { entries, .. } => {
1384 assert_eq!(entries.len(), 2);
1385 assert!(matches!(entries[0].value, EnumValueSrc::Literal(10)));
1386 match &entries[1].value {
1387 EnumValueSrc::FromNode(node) => assert_eq!(node, "RegModeVal"),
1388 other => panic!("unexpected entry value: {other:?}"),
1389 }
1390 }
1391 other => panic!("unexpected node: {other:?}"),
1392 }
1393 }
1394
1395 #[test]
1402 fn address_terms_are_summed() {
1403 const XML: &str = r#"
1404 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1405 <Integer Name="RegAddr">
1406 <Address>0x2000</Address>
1407 <Length>4</Length>
1408 <AccessMode>RW</AccessMode>
1409 <Min>0</Min>
1410 <Max>65535</Max>
1411 </Integer>
1412 <Integer Name="Gain">
1413 <pAddress>RegAddr</pAddress>
1414 <Address>0x00000008</Address>
1415 <Length>4</Length>
1416 <AccessMode>RW</AccessMode>
1417 <Min>0</Min>
1418 <Max>255</Max>
1419 </Integer>
1420 </RegisterDescription>
1421 "#;
1422
1423 let model = parse(XML).expect("parse indirect xml");
1424 assert_eq!(model.nodes.len(), 2);
1425 match &model.nodes[0] {
1426 NodeDecl::Integer {
1427 name, addressing, ..
1428 } => {
1429 assert_eq!(name, "RegAddr");
1430 assert_eq!(addressing.as_ref(), Some(&Addressing::fixed(0x2000, 4)));
1431 }
1432 other => panic!("unexpected node: {other:?}"),
1433 }
1434 match &model.nodes[1] {
1435 NodeDecl::Integer {
1436 name, addressing, ..
1437 } => {
1438 assert_eq!(name, "Gain");
1439 match addressing {
1440 Some(Addressing::Sum { terms, len }) => {
1441 assert_eq!(
1442 terms,
1443 &[AddressTerm::Node("RegAddr".into()), AddressTerm::Fixed(0x8),]
1444 );
1445 assert_eq!(*len, 4);
1446 }
1447 other => panic!("expected summed addressing, got {other:?}"),
1448 }
1449 }
1450 other => panic!("unexpected node: {other:?}"),
1451 }
1452 }
1453
1454 #[test]
1456 fn parse_p_index_term() {
1457 const XML: &str = r#"
1458 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1459 <MaskedIntReg Name="RegTriggerInqDelay">
1460 <Address>0x13400</Address>
1461 <pIndex Offset="64">IntTriggerSelector</pIndex>
1462 <Length>4</Length>
1463 <AccessMode>RO</AccessMode>
1464 <Bit>30</Bit>
1465 </MaskedIntReg>
1466 </RegisterDescription>
1467 "#;
1468
1469 let model = parse(XML).expect("parse pIndex xml");
1470 match &model.nodes[0] {
1471 NodeDecl::Integer { addressing, .. } => match addressing {
1472 Some(Addressing::Sum { terms, .. }) => assert_eq!(
1473 terms,
1474 &[
1475 AddressTerm::Fixed(0x13400),
1476 AddressTerm::Index {
1477 node: "IntTriggerSelector".into(),
1478 offset: IndexOffset::Fixed(64),
1479 },
1480 ]
1481 ),
1482 other => panic!("expected summed addressing, got {other:?}"),
1483 },
1484 other => panic!("unexpected node: {other:?}"),
1485 }
1486 }
1487
1488 #[test]
1494 fn struct_reg_inherits_all_address_terms() {
1495 const XML: &str = r#"
1496 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1497 <StructReg Comment="Gain Inquiry Register">
1498 <Address>0x520</Address>
1499 <pAddress>CamRegBaseAddress</pAddress>
1500 <Length>4</Length>
1501 <AccessMode>RO</AccessMode>
1502 <Endianess>BigEndian</Endianess>
1503 <StructEntry Name="GainPresInq_Bit"><Bit>0</Bit></StructEntry>
1504 <StructEntry Name="GainAutoInq_Bit"><Bit>6</Bit></StructEntry>
1505 </StructReg>
1506 </RegisterDescription>
1507 "#;
1508
1509 let model = parse(XML).expect("parse struct reg");
1510 assert_eq!(model.nodes.len(), 2);
1511 for node in &model.nodes {
1512 match node {
1513 NodeDecl::Integer { addressing, .. } => match addressing {
1514 Some(Addressing::Sum { terms, len }) => {
1515 assert_eq!(
1516 terms,
1517 &[
1518 AddressTerm::Fixed(0x520),
1519 AddressTerm::Node("CamRegBaseAddress".into()),
1520 ]
1521 );
1522 assert_eq!(*len, 4);
1523 }
1524 other => panic!("expected summed addressing, got {other:?}"),
1525 },
1526 other => panic!("unexpected node: {other:?}"),
1527 }
1528 }
1529 }
1530
1531 #[test]
1534 fn struct_reg_without_address_is_skipped() {
1535 const XML: &str = r#"
1536 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1537 <StructReg>
1538 <Length>4</Length>
1539 <AccessMode>RO</AccessMode>
1540 <StructEntry Name="SomeInq_Bit"><Bit>0</Bit></StructEntry>
1541 </StructReg>
1542 </RegisterDescription>
1543 "#;
1544
1545 let model = parse(XML).expect("document still parses");
1546 assert!(model.nodes.is_empty());
1547 assert_eq!(model.skipped.len(), 1);
1548 assert!(model.skipped[0].error.contains("Address"));
1549 }
1550
1551 #[test]
1552 fn parse_indirect_float_is_scaled_integer() {
1553 const XML: &str = r#"
1559 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1560 <Integer Name="RegAddr">
1561 <Address>0x2000</Address>
1562 <Length>4</Length>
1563 <AccessMode>RW</AccessMode>
1564 </Integer>
1565 <Float Name="Exposure">
1566 <pAddress>RegAddr</pAddress>
1567 <Length>4</Length>
1568 <AccessMode>RW</AccessMode>
1569 </Float>
1570 </RegisterDescription>
1571 "#;
1572
1573 let model = parse(XML).expect("parse indirect float");
1574 let float = model
1575 .nodes
1576 .iter()
1577 .find(|n| matches!(n, NodeDecl::Float { name, .. } if name == "Exposure"))
1578 .expect("Exposure node");
1579 match float {
1580 NodeDecl::Float {
1581 encoding,
1582 addressing,
1583 ..
1584 } => {
1585 assert!(
1586 matches!(
1587 addressing,
1588 Some(Addressing::Sum { terms, .. })
1589 if terms.iter().any(|t| matches!(t, AddressTerm::Node(_)))
1590 ),
1591 "expected a pAddress term"
1592 );
1593 assert_eq!(*encoding, FloatEncoding::ScaledInteger);
1594 }
1595 _ => unreachable!(),
1596 }
1597 }
1598
1599 #[test]
1600 fn parse_integer_bitfield_big_endian() {
1601 const XML: &str = r#"
1602 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1603 <Integer Name="Packed">
1604 <Address>0x1000</Address>
1605 <Length>4</Length>
1606 <AccessMode>RW</AccessMode>
1607 <Min>0</Min>
1608 <Max>65535</Max>
1609 <Lsb>8</Lsb>
1610 <Msb>15</Msb>
1611 <Endianness>BigEndian</Endianness>
1612 </Integer>
1613 </RegisterDescription>
1614 "#;
1615
1616 let model = parse(XML).expect("parse big-endian bitfield");
1617 assert_eq!(model.nodes.len(), 1);
1618 match &model.nodes[0] {
1619 NodeDecl::Integer { len, bitfield, .. } => {
1620 assert_eq!(*len, 4);
1621 let field = bitfield.as_ref().expect("bitfield present");
1622 assert_eq!(field.byte_order, ByteOrder::Big);
1623 assert_eq!(field.bit_length, 8);
1624 assert_eq!(field.bit_offset, 16);
1625 }
1626 other => panic!("unexpected node: {other:?}"),
1627 }
1628 }
1629
1630 #[test]
1631 fn parse_boolean_bitfield_default_length() {
1632 const XML: &str = r#"
1633 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1634 <Boolean Name="Flag">
1635 <Address>0x2000</Address>
1636 <Length>1</Length>
1637 <AccessMode>RW</AccessMode>
1638 <Bit>3</Bit>
1639 </Boolean>
1640 </RegisterDescription>
1641 "#;
1642
1643 let model = parse(XML).expect("parse boolean bitfield");
1644 assert_eq!(model.nodes.len(), 1);
1645 match &model.nodes[0] {
1646 NodeDecl::Boolean { len, bitfield, .. } => {
1647 assert_eq!(*len, 1);
1648 let bf = bitfield.as_ref().expect("bitfield present");
1649 assert_eq!(bf.byte_order, ByteOrder::Little);
1650 assert_eq!(bf.bit_length, 1);
1651 assert_eq!(bf.bit_offset, 3);
1652 }
1653 other => panic!("unexpected node: {other:?}"),
1654 }
1655 }
1656
1657 #[test]
1658 fn parse_integer_bitfield_mask() {
1659 const XML: &str = r#"
1660 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1661 <Integer Name="Masked">
1662 <Address>0x3000</Address>
1663 <Length>4</Length>
1664 <AccessMode>RW</AccessMode>
1665 <Min>0</Min>
1666 <Max>65535</Max>
1667 <Mask>0x0000FF00</Mask>
1668 </Integer>
1669 </RegisterDescription>
1670 "#;
1671
1672 let model = parse(XML).expect("parse mask bitfield");
1673 assert_eq!(model.nodes.len(), 1);
1674 match &model.nodes[0] {
1675 NodeDecl::Integer { bitfield, .. } => {
1676 let field = bitfield.as_ref().expect("bitfield present");
1677 assert_eq!(field.byte_order, ByteOrder::Little);
1678 assert_eq!(field.bit_length, 8);
1679 assert_eq!(field.bit_offset, 8);
1680 }
1681 other => panic!("unexpected node: {other:?}"),
1682 }
1683 }
1684
1685 #[test]
1686 fn parse_node_metadata() {
1687 const XML: &str = r#"
1688 <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1689 <Integer Name="Width">
1690 <Address>0x100</Address>
1691 <Length>4</Length>
1692 <AccessMode>RW</AccessMode>
1693 <Min>16</Min>
1694 <Max>4096</Max>
1695 <Visibility>Expert</Visibility>
1696 <Description>Image width in pixels.</Description>
1697 <ToolTip>Width of the acquired image</ToolTip>
1698 <DisplayName>Image Width</DisplayName>
1699 <Representation>Linear</Representation>
1700 </Integer>
1701 <Float Name="Gain">
1702 <Address>0x200</Address>
1703 <Length>4</Length>
1704 <AccessMode>RW</AccessMode>
1705 <Min>0.0</Min>
1706 <Max>48.0</Max>
1707 <Unit>dB</Unit>
1708 <Visibility>Beginner</Visibility>
1709 <Representation>Logarithmic</Representation>
1710 </Float>
1711 <Category Name="Root">
1712 <Visibility>Guru</Visibility>
1713 <Description>Top-level category</Description>
1714 <pFeature>Width</pFeature>
1715 <pFeature>Gain</pFeature>
1716 </Category>
1717 <Enumeration Name="PixelFormat">
1718 <Address>0x300</Address>
1719 <Length>4</Length>
1720 <AccessMode>RW</AccessMode>
1721 <Visibility>Beginner</Visibility>
1722 <ToolTip>Pixel format selector</ToolTip>
1723 <EnumEntry Name="Mono8" Value="0" />
1724 </Enumeration>
1725 </RegisterDescription>
1726 "#;
1727
1728 let model = parse(XML).expect("parse metadata xml");
1729 assert_eq!(model.nodes.len(), 4);
1730
1731 match &model.nodes[0] {
1733 NodeDecl::Integer { name, meta, .. } => {
1734 assert_eq!(name, "Width");
1735 assert_eq!(meta.visibility, Visibility::Expert);
1736 assert_eq!(meta.description.as_deref(), Some("Image width in pixels."));
1737 assert_eq!(meta.tooltip.as_deref(), Some("Width of the acquired image"));
1738 assert_eq!(meta.display_name.as_deref(), Some("Image Width"));
1739 assert_eq!(meta.representation, Some(Representation::Linear));
1740 }
1741 other => panic!("unexpected node: {other:?}"),
1742 }
1743
1744 match &model.nodes[1] {
1746 NodeDecl::Float { name, meta, .. } => {
1747 assert_eq!(name, "Gain");
1748 assert_eq!(meta.visibility, Visibility::Beginner);
1749 assert_eq!(meta.representation, Some(Representation::Logarithmic));
1750 assert!(meta.description.is_none());
1751 }
1752 other => panic!("unexpected node: {other:?}"),
1753 }
1754
1755 match &model.nodes[2] {
1757 NodeDecl::Category { name, meta, .. } => {
1758 assert_eq!(name, "Root");
1759 assert_eq!(meta.visibility, Visibility::Guru);
1760 assert_eq!(meta.description.as_deref(), Some("Top-level category"));
1761 }
1762 other => panic!("unexpected node: {other:?}"),
1763 }
1764
1765 match &model.nodes[3] {
1767 NodeDecl::Enum { name, meta, .. } => {
1768 assert_eq!(name, "PixelFormat");
1769 assert_eq!(meta.visibility, Visibility::Beginner);
1770 assert_eq!(meta.tooltip.as_deref(), Some("Pixel format selector"));
1771 }
1772 other => panic!("unexpected node: {other:?}"),
1773 }
1774 }
1775
1776 fn parse_fragment(node: &str) -> XmlModel {
1778 let xml = format!(
1779 r#"<RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="1" SchemaSubMinorVersion="0">{node}</RegisterDescription>"#
1780 );
1781 parse(&xml).expect("parse fragment")
1782 }
1783
1784 fn only_meta(model: &XmlModel) -> &NodeMeta {
1786 match model.nodes.first().expect("one node") {
1787 NodeDecl::Integer { meta, .. } => meta,
1788 other => panic!("unexpected node: {other:?}"),
1789 }
1790 }
1791
1792 #[test]
1796 fn cdata_text_is_taken_literally() {
1797 let model = parse_fragment(
1798 r#"<Integer Name="Gain">
1799 <Address>0x100</Address>
1800 <ToolTip><![CDATA[Gain in dB & raw units, 0 < x < 10]]></ToolTip>
1801 </Integer>"#,
1802 );
1803 assert_eq!(
1804 only_meta(&model).tooltip.as_deref(),
1805 Some("Gain in dB & raw units, 0 < x < 10")
1806 );
1807 }
1808
1809 #[test]
1812 fn dangling_ampersand_is_kept_verbatim() {
1813 let model = parse_fragment(
1814 r#"<Integer Name="Gain">
1815 <Address>0x100</Address>
1816 <ToolTip>Exposure & gain</ToolTip>
1817 </Integer>"#,
1818 );
1819 assert_eq!(
1820 only_meta(&model).tooltip.as_deref(),
1821 Some("Exposure & gain")
1822 );
1823 }
1824
1825 #[test]
1828 fn comments_inside_text_elements_are_dropped() {
1829 let model = parse_fragment(
1830 r#"<Integer Name="Gain">
1831 <Address>0x100</Address>
1832 <Description>Analog <!-- R&D note --> gain</Description>
1833 </Integer>"#,
1834 );
1835 assert_eq!(
1836 only_meta(&model).description.as_deref(),
1837 Some("Analog gain")
1838 );
1839 }
1840
1841 #[test]
1844 fn entity_references_resolve_and_preserve_spacing() {
1845 let model = parse_fragment(
1846 r#"<Integer Name="Gain">
1847 <Address>0x100</Address>
1848 <ToolTip>A & B < C > D "E" 'F'</ToolTip>
1849 </Integer>"#,
1850 );
1851 assert_eq!(
1852 only_meta(&model).tooltip.as_deref(),
1853 Some(r#"A & B < C > D "E" 'F'"#)
1854 );
1855 }
1856
1857 #[test]
1859 fn character_references_resolve() {
1860 let model = parse_fragment(
1861 r#"<Integer Name="Gain">
1862 <Address>0x100</Address>
1863 <ToolTip>— dash —</ToolTip>
1864 </Integer>"#,
1865 );
1866 assert_eq!(only_meta(&model).tooltip.as_deref(), Some("— dash —"));
1867 }
1868
1869 #[test]
1872 fn unknown_entity_is_kept_as_written() {
1873 let model = parse_fragment(
1874 r#"<Integer Name="Gain">
1875 <Address>0x100</Address>
1876 <ToolTip>copyright © vendor</ToolTip>
1877 </Integer>"#,
1878 );
1879 assert_eq!(
1880 only_meta(&model).tooltip.as_deref(),
1881 Some("copyright © vendor")
1882 );
1883 }
1884
1885 #[test]
1888 fn swissknife_formula_entities_resolve_to_operators() {
1889 let model = parse_fragment(
1890 r#"<IntSwissKnife Name="GainMask">
1891 <pVariable Name="RAW">Gain</pVariable>
1892 <Formula>(RAW & 0xFF) < 16</Formula>
1893 </IntSwissKnife>"#,
1894 );
1895 match model.nodes.first().expect("one node") {
1896 NodeDecl::SwissKnife(node) => assert_eq!(node.expr, "(RAW & 0xFF) < 16"),
1897 other => panic!("unexpected node: {other:?}"),
1898 }
1899 }
1900
1901 #[test]
1905 fn swissknife_without_variables_is_accepted() {
1906 let model = parse_fragment(
1907 r#"<IntSwissKnife Name="PixelDynamicRangeMin_Value">
1908 <Formula>0x1234</Formula>
1909 </IntSwissKnife>"#,
1910 );
1911 match model.nodes.first().expect("one node") {
1912 NodeDecl::SwissKnife(node) => {
1913 assert_eq!(node.expr, "0x1234");
1914 assert!(node.variables.is_empty());
1915 }
1916 other => panic!("unexpected node: {other:?}"),
1917 }
1918 }
1919
1920 #[test]
1923 fn access_mode_accepts_aliases_and_defaults_unknown_to_rw() {
1924 assert_eq!(AccessMode::parse("R").unwrap(), AccessMode::RO);
1925 assert_eq!(AccessMode::parse("W").unwrap(), AccessMode::WO);
1926 assert_eq!(AccessMode::parse("WR").unwrap(), AccessMode::RW);
1927 assert_eq!(AccessMode::parse(" ro ").unwrap(), AccessMode::RO);
1928 assert_eq!(AccessMode::parse("Bogus").unwrap(), AccessMode::RW);
1929 }
1930
1931 #[test]
1934 fn unparsable_node_is_skipped_not_fatal() {
1935 let model = parse_fragment(
1936 r#"<Integer Name="Before">
1937 <Address>0x100</Address>
1938 <Length>4</Length>
1939 </Integer>
1940 <Integer Name="Broken">
1941 <Address>0xZZZZ</Address>
1942 <Length>4</Length>
1943 </Integer>
1944 <Integer Name="After">
1945 <Address>0x200</Address>
1946 <Length>4</Length>
1947 </Integer>"#,
1948 );
1949
1950 let names: Vec<&str> = model
1951 .nodes
1952 .iter()
1953 .map(|node| match node {
1954 NodeDecl::Integer { name, .. } => name.as_str(),
1955 other => panic!("unexpected node: {other:?}"),
1956 })
1957 .collect();
1958 assert_eq!(names, ["Before", "After"]);
1959
1960 assert_eq!(model.skipped.len(), 1);
1961 let skipped = &model.skipped[0];
1962 assert_eq!(skipped.tag, "Integer");
1963 assert_eq!(skipped.name.as_deref(), Some("Broken"));
1964 assert!(
1965 skipped.error.contains("invalid hex"),
1966 "unexpected error: {}",
1967 skipped.error
1968 );
1969 }
1970
1971 #[test]
1975 fn node_missing_required_name_is_skipped() {
1976 let model = parse_fragment(
1977 r#"<Integer>
1978 <Address>0x100</Address>
1979 </Integer>
1980 <Integer Name="Good">
1981 <Address>0x200</Address>
1982 <Length>4</Length>
1983 </Integer>"#,
1984 );
1985 assert_eq!(model.nodes.len(), 1);
1986 assert_eq!(model.skipped.len(), 1);
1987 assert_eq!(model.skipped[0].name, None);
1988 }
1989
1990 #[test]
1992 fn clean_document_skips_nothing() {
1993 let model = parse(FIXTURE).expect("parse fixture");
1994 assert!(model.skipped.is_empty());
1995 }
1996}