Skip to main content

viva_genapi_xml/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2//! Load and pre-parse GenICam XML using quick-xml.
3//!
4//! This crate provides types and functions for parsing GenICam XML descriptions
5//! into a structured representation that can be used by the core evaluation engine.
6
7mod 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 serde::{Deserialize, Serialize};
19use thiserror::Error;
20
21use parsers::{
22    parse_boolean, parse_category, parse_category_empty, parse_command, parse_command_empty,
23    parse_converter, parse_enum, parse_float, parse_int_converter, parse_integer, parse_string,
24    parse_struct_reg, parse_swissknife,
25};
26use util::{attribute_value, skip_element};
27
28/// Source of the numeric value backing an enumeration entry.
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub enum EnumValueSrc {
31    /// Numeric literal declared directly in the XML.
32    Literal(i64),
33    /// Value obtained from another node referenced via `<pValue>`.
34    FromNode(String),
35}
36
37/// Declaration for a single enumeration entry.
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub struct EnumEntryDecl {
40    /// Symbolic entry name exposed to clients.
41    pub name: String,
42    /// Source describing how to resolve the numeric value for this entry.
43    pub value: EnumValueSrc,
44    /// Optional user facing label.
45    pub display_name: Option<String>,
46}
47
48#[derive(Debug, Error)]
49#[non_exhaustive]
50pub enum XmlError {
51    #[error("xml: {0}")]
52    Xml(String),
53    #[error("invalid descriptor: {0}")]
54    Invalid(String),
55    #[error("transport: {0}")]
56    Transport(String),
57    #[error("unsupported URL: {0}")]
58    Unsupported(String),
59}
60
61/// Visibility level controlling which users see a feature.
62///
63/// GenICam defines four levels; features at a given level are visible to
64/// users at that level and above.
65#[derive(
66    Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
67)]
68#[non_exhaustive]
69pub enum Visibility {
70    /// Shown to all users (default).
71    #[default]
72    Beginner,
73    /// Shown to experienced users.
74    Expert,
75    /// Shown only to advanced integrators.
76    Guru,
77    /// Hidden from all UI presentations.
78    Invisible,
79}
80
81impl Visibility {
82    pub(crate) fn parse(s: &str) -> Option<Self> {
83        match s.trim() {
84            "Beginner" => Some(Self::Beginner),
85            "Expert" => Some(Self::Expert),
86            "Guru" => Some(Self::Guru),
87            "Invisible" => Some(Self::Invisible),
88            _ => None,
89        }
90    }
91}
92
93/// Recommended UI representation for a numeric feature.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
95#[non_exhaustive]
96pub enum Representation {
97    Linear,
98    Logarithmic,
99    Boolean,
100    PureNumber,
101    HexNumber,
102    /// Display as dotted-quad IPv4 address.
103    IPV4Address,
104    /// Display as colon-separated MAC address.
105    MACAddress,
106}
107
108impl Representation {
109    pub(crate) fn parse(s: &str) -> Option<Self> {
110        match s.trim() {
111            "Linear" => Some(Self::Linear),
112            "Logarithmic" => Some(Self::Logarithmic),
113            "Boolean" => Some(Self::Boolean),
114            "PureNumber" => Some(Self::PureNumber),
115            "HexNumber" => Some(Self::HexNumber),
116            "IPV4Address" => Some(Self::IPV4Address),
117            "MACAddress" => Some(Self::MACAddress),
118            _ => None,
119        }
120    }
121}
122
123/// Shared metadata present on every GenICam node.
124#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
125pub struct NodeMeta {
126    /// Visibility level (Beginner, Expert, Guru, Invisible).
127    pub visibility: Visibility,
128    /// Long-form description of the feature.
129    pub description: Option<String>,
130    /// Short tooltip text for UI hover hints.
131    pub tooltip: Option<String>,
132    /// Human-readable label (may differ from the node name).
133    pub display_name: Option<String>,
134    /// Recommended UI representation for numeric features.
135    pub representation: Option<Representation>,
136}
137
138/// Access privileges for a GenICam node as described in the XML.
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
140pub enum AccessMode {
141    /// Read-only node. The underlying register must not be modified by the client.
142    RO,
143    /// Write-only node. Reading the register is not permitted.
144    WO,
145    /// Read-write node. The register may be read and written by the client.
146    RW,
147}
148
149impl AccessMode {
150    pub(crate) fn parse(value: &str) -> Result<Self, XmlError> {
151        match value.trim().to_ascii_uppercase().as_str() {
152            "RO" => Ok(AccessMode::RO),
153            "WO" => Ok(AccessMode::WO),
154            "RW" => Ok(AccessMode::RW),
155            other => Err(XmlError::Invalid(format!("unknown access mode: {other}"))),
156        }
157    }
158}
159
160/// Register addressing metadata for a node.
161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
162pub enum Addressing {
163    /// Node uses a fixed register block regardless of selector state.
164    Fixed { address: u64, len: u32 },
165    /// Node switches between register blocks based on a selector value.
166    BySelector {
167        /// Name of the selector node controlling the address.
168        selector: String,
169        /// Mapping of selector value to `(address, length)` pair.
170        map: Vec<(String, (u64, u32))>,
171    },
172    /// Node resolves its register block through another node providing the address.
173    Indirect {
174        /// Node providing the register address at runtime.
175        p_address_node: String,
176        /// Length of the target register block in bytes.
177        len: u32,
178    },
179}
180
181/// Byte order used to interpret a multi-byte register payload.
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
183pub enum ByteOrder {
184    /// The first byte contains the least significant bits.
185    Little,
186    /// The first byte contains the most significant bits.
187    Big,
188}
189
190impl ByteOrder {
191    pub(crate) fn parse(tag: &str) -> Option<Self> {
192        match tag.trim().to_ascii_lowercase().as_str() {
193            "littleendian" => Some(ByteOrder::Little),
194            "bigendian" => Some(ByteOrder::Big),
195            _ => None,
196        }
197    }
198}
199
200/// Bitfield metadata describing a sub-range of a register payload.
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
202pub struct BitField {
203    /// Starting bit offset within the interpreted register value.
204    pub bit_offset: u16,
205    /// Number of bits covered by the field.
206    pub bit_length: u16,
207    /// Byte order used when interpreting the enclosing register.
208    pub byte_order: ByteOrder,
209}
210
211/// Output type of a SwissKnife expression node.
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
213pub enum SkOutput {
214    /// Integer output. The runtime rounds the computed value to the nearest
215    /// integer with ties going towards zero.
216    Integer,
217    /// Floating point output. The runtime exposes the value as a `f64` without
218    /// any additional processing.
219    #[default]
220    Float,
221}
222
223impl SkOutput {
224    pub(crate) fn parse(tag: &str) -> Option<Self> {
225        match tag.trim().to_ascii_lowercase().as_str() {
226            "integer" => Some(SkOutput::Integer),
227            "float" => Some(SkOutput::Float),
228            _ => None,
229        }
230    }
231}
232
233/// Declaration of a SwissKnife node consisting of an arithmetic expression.
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct SwissKnifeDecl {
236    /// Feature name exposed to clients.
237    pub name: String,
238    /// Shared metadata.
239    pub meta: NodeMeta,
240    /// Raw expression string to be parsed by the runtime.
241    pub expr: String,
242    /// Mapping of variables used in the expression to provider node names.
243    pub variables: Vec<(String, String)>,
244    /// Desired output type (integer or float).
245    pub output: SkOutput,
246}
247
248/// Declaration of a Converter node for bidirectional value transformation.
249///
250/// Converters expose a floating-point value computed from an underlying
251/// register or node via a formula.
252#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct ConverterDecl {
254    /// Feature name exposed to clients.
255    pub name: String,
256    /// Shared metadata.
257    pub meta: NodeMeta,
258    /// Name of the node providing the raw register value.
259    pub p_value: String,
260    /// Expression converting raw register value to user-facing value (FROM direction).
261    pub formula_to: String,
262    /// Expression converting user-facing value back to raw register value (TO direction).
263    pub formula_from: String,
264    /// Mapping of expression variables to provider node names for `formula_to`.
265    pub variables_to: Vec<(String, String)>,
266    /// Mapping of expression variables to provider node names for `formula_from`.
267    pub variables_from: Vec<(String, String)>,
268    /// Engineering unit (if provided).
269    pub unit: Option<String>,
270    /// Desired output type.
271    pub output: SkOutput,
272}
273
274/// Declaration of an IntConverter node for integer-specific bidirectional conversion.
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct IntConverterDecl {
277    /// Feature name exposed to clients.
278    pub name: String,
279    /// Shared metadata.
280    pub meta: NodeMeta,
281    /// Name of the node providing the raw register value.
282    pub p_value: String,
283    /// Expression converting raw register value to user-facing value (FROM direction).
284    pub formula_to: String,
285    /// Expression converting user-facing value back to raw register value (TO direction).
286    pub formula_from: String,
287    /// Mapping of expression variables to provider node names for `formula_to`.
288    pub variables_to: Vec<(String, String)>,
289    /// Mapping of expression variables to provider node names for `formula_from`.
290    pub variables_from: Vec<(String, String)>,
291    /// Engineering unit (if provided).
292    pub unit: Option<String>,
293}
294
295/// Declaration of a StringReg node for string-typed register access.
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct StringDecl {
298    /// Feature name exposed to clients.
299    pub name: String,
300    /// Shared metadata.
301    pub meta: NodeMeta,
302    /// Addressing metadata for the register block.
303    pub addressing: Addressing,
304    /// Access privileges.
305    pub access: AccessMode,
306}
307
308/// Declaration of a node extracted from the GenICam XML description.
309#[derive(Debug, Clone, Serialize, Deserialize)]
310pub enum NodeDecl {
311    /// Integer feature backed by a register block or delegated via pValue.
312    Integer {
313        /// Feature name.
314        name: String,
315        /// Shared metadata (visibility, description, tooltip, etc.).
316        meta: NodeMeta,
317        /// Addressing metadata (absent when delegated via `pvalue`).
318        addressing: Option<Addressing>,
319        /// Length in bytes of the register payload.
320        len: u32,
321        /// Access privileges.
322        access: AccessMode,
323        /// Minimum allowed user value.
324        min: i64,
325        /// Maximum allowed user value.
326        max: i64,
327        /// Optional increment step enforced by the device.
328        inc: Option<i64>,
329        /// Engineering unit (if provided).
330        unit: Option<String>,
331        /// Optional bitfield metadata describing the active bit range.
332        bitfield: Option<BitField>,
333        /// Selector nodes referencing this feature.
334        selectors: Vec<String>,
335        /// Selector gating rules in the form (selector name, allowed values).
336        selected_if: Vec<(String, Vec<String>)>,
337        /// Node providing the value (delegates read/write to another node).
338        pvalue: Option<String>,
339        /// Node providing the dynamic maximum.
340        p_max: Option<String>,
341        /// Node providing the dynamic minimum.
342        p_min: Option<String>,
343        /// Static value (for constant integer nodes with `<Value>`).
344        value: Option<i64>,
345    },
346    /// Floating point feature backed by an integer register with scaling
347    /// or delegated via pValue.
348    Float {
349        name: String,
350        meta: NodeMeta,
351        /// Addressing metadata (absent when delegated via `pvalue`).
352        addressing: Option<Addressing>,
353        access: AccessMode,
354        min: f64,
355        max: f64,
356        unit: Option<String>,
357        /// Optional rational scale applied to the raw register value.
358        scale: Option<(i64, i64)>,
359        /// Optional additive offset applied after scaling.
360        offset: Option<f64>,
361        selectors: Vec<String>,
362        selected_if: Vec<(String, Vec<String>)>,
363        /// Node providing the value (delegates read/write to another node).
364        pvalue: Option<String>,
365    },
366    /// Enumeration feature exposing a list of named integer values.
367    Enum {
368        name: String,
369        meta: NodeMeta,
370        /// Addressing metadata (absent when delegated via `pvalue`).
371        addressing: Option<Addressing>,
372        access: AccessMode,
373        entries: Vec<EnumEntryDecl>,
374        default: Option<String>,
375        selectors: Vec<String>,
376        selected_if: Vec<(String, Vec<String>)>,
377        /// Node providing the integer value (delegates register read/write).
378        pvalue: Option<String>,
379    },
380    /// Boolean feature backed by a single bit/byte register or delegated via pValue.
381    Boolean {
382        name: String,
383        meta: NodeMeta,
384        /// Addressing metadata (absent when delegated via `pvalue`).
385        addressing: Option<Addressing>,
386        len: u32,
387        access: AccessMode,
388        bitfield: Option<BitField>,
389        selectors: Vec<String>,
390        selected_if: Vec<(String, Vec<String>)>,
391        /// Node providing the value (delegates read/write to another node).
392        pvalue: Option<String>,
393        /// On value for pValue-backed booleans.
394        on_value: Option<i64>,
395        /// Off value for pValue-backed booleans.
396        off_value: Option<i64>,
397    },
398    /// Command feature that triggers an action when written.
399    Command {
400        name: String,
401        meta: NodeMeta,
402        /// Fixed register address (absent when delegated via `pvalue`).
403        address: Option<u64>,
404        len: u32,
405        /// Node providing the command register (delegates write).
406        pvalue: Option<String>,
407        /// Value to write when executing the command.
408        command_value: Option<i64>,
409    },
410    /// Category used to organise features.
411    Category {
412        name: String,
413        meta: NodeMeta,
414        children: Vec<String>,
415    },
416    /// Computed value backed by an arithmetic expression referencing other nodes.
417    SwissKnife(SwissKnifeDecl),
418    /// Converter transforming raw values to/from user-facing floating-point values.
419    Converter(ConverterDecl),
420    /// IntConverter transforming raw values to/from user-facing integer values.
421    IntConverter(IntConverterDecl),
422    /// StringReg for string-typed register access.
423    String(StringDecl),
424}
425
426/// Full XML model describing the GenICam schema version and all declared nodes.
427#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct XmlModel {
429    /// Combined schema version extracted from the RegisterDescription attributes.
430    pub version: String,
431    /// Flat list of node declarations present in the document.
432    pub nodes: Vec<NodeDecl>,
433}
434
435/// Minimal metadata extracted from a quick XML scan.
436#[derive(Debug, Clone, PartialEq, Eq)]
437pub struct MinimalXmlInfo {
438    pub schema_version: Option<String>,
439    pub top_level_features: Vec<String>,
440}
441
442/// Parse a GenICam XML snippet and collect minimal metadata.
443pub fn parse_into_minimal_nodes(xml: &str) -> Result<MinimalXmlInfo, XmlError> {
444    let mut reader = Reader::from_str(xml);
445    reader.trim_text(true);
446    let mut buf = Vec::new();
447    let mut depth = 0usize;
448    let mut schema_version: Option<String> = None;
449    let mut top_level_features = Vec::new();
450
451    loop {
452        match reader.read_event_into(&mut buf) {
453            Ok(Event::Start(e)) => {
454                depth += 1;
455                handle_start(&e, depth, &mut schema_version, &mut top_level_features)?;
456            }
457            Ok(Event::Empty(e)) => {
458                depth += 1;
459                handle_start(&e, depth, &mut schema_version, &mut top_level_features)?;
460                if depth > 0 {
461                    depth = depth.saturating_sub(1);
462                }
463            }
464            Ok(Event::End(_)) => {
465                if depth > 0 {
466                    depth = depth.saturating_sub(1);
467                }
468            }
469            Ok(Event::Eof) => break,
470            Err(err) => return Err(XmlError::Xml(err.to_string())),
471            _ => {}
472        }
473        buf.clear();
474    }
475
476    Ok(MinimalXmlInfo {
477        schema_version,
478        top_level_features,
479    })
480}
481
482/// Parse a GenICam XML document into an [`XmlModel`].
483///
484/// The parser only understands a practical subset of the schema. Unknown tags
485/// are skipped which keeps the implementation forward compatible with richer
486/// documents.
487pub fn parse(xml: &str) -> Result<XmlModel, XmlError> {
488    let mut reader = Reader::from_str(xml);
489    reader.trim_text(true);
490    let mut buf = Vec::new();
491    let mut version = String::from("0.0.0");
492    let mut nodes = Vec::new();
493
494    loop {
495        match reader.read_event_into(&mut buf) {
496            Ok(Event::Start(ref e)) => match e.name().as_ref() {
497                b"RegisterDescription" => {
498                    version = schema_version_from(e)?;
499                }
500                b"Integer" | b"IntReg" | b"MaskedIntReg" => {
501                    let node = parse_integer(&mut reader, e.clone())?;
502                    nodes.push(node);
503                }
504                b"IntSwissKnife" => {
505                    let node = parse_swissknife(&mut reader, e.clone())?;
506                    nodes.push(node);
507                }
508                b"Float" | b"FloatReg" => {
509                    let node = parse_float(&mut reader, e.clone())?;
510                    nodes.push(node);
511                }
512                b"Enumeration" => {
513                    let node = parse_enum(&mut reader, e.clone())?;
514                    nodes.push(node);
515                }
516                b"Boolean" => {
517                    let node = parse_boolean(&mut reader, e.clone())?;
518                    nodes.push(node);
519                }
520                b"Command" => {
521                    let node = parse_command(&mut reader, e.clone())?;
522                    nodes.push(node);
523                }
524                b"Category" => {
525                    let node = parse_category(&mut reader, e.clone())?;
526                    nodes.push(node);
527                }
528                b"SwissKnife" => {
529                    let node = parse_swissknife(&mut reader, e.clone())?;
530                    nodes.push(node);
531                }
532                b"Converter" => {
533                    let node = parse_converter(&mut reader, e.clone())?;
534                    nodes.push(node);
535                }
536                b"IntConverter" => {
537                    let node = parse_int_converter(&mut reader, e.clone())?;
538                    nodes.push(node);
539                }
540                b"StringReg" | b"String" => {
541                    let node = parse_string(&mut reader, e.clone())?;
542                    nodes.push(node);
543                }
544                b"StructReg" => {
545                    let entries = parse_struct_reg(&mut reader, e.clone())?;
546                    nodes.extend(entries);
547                }
548                b"Group" => {
549                    // Group is a transparent container wrapping feature nodes;
550                    // let child events surface in the next loop iterations.
551                }
552                b"Port" => {
553                    // Port nodes are transport-level abstractions; skip them.
554                    skip_element(&mut reader, e.name().as_ref())?;
555                }
556                _ => {
557                    skip_element(&mut reader, e.name().as_ref())?;
558                }
559            },
560            Ok(Event::Empty(ref e)) => match e.name().as_ref() {
561                b"RegisterDescription" => {
562                    version = schema_version_from(e)?;
563                }
564                b"Command" => {
565                    let node = parse_command_empty(e)?;
566                    nodes.push(node);
567                }
568                b"Category" => {
569                    let node = parse_category_empty(e)?;
570                    nodes.push(node);
571                }
572                _ => {}
573            },
574            Ok(Event::Eof) => break,
575            Err(err) => return Err(XmlError::Xml(err.to_string())),
576            _ => {}
577        }
578        buf.clear();
579    }
580
581    Ok(XmlModel { version, nodes })
582}
583
584fn schema_version_from(event: &BytesStart<'_>) -> Result<String, XmlError> {
585    let major = attribute_value(event, b"SchemaMajorVersion")?;
586    let minor = attribute_value(event, b"SchemaMinorVersion")?;
587    let sub = attribute_value(event, b"SchemaSubMinorVersion")?;
588    let major = major.unwrap_or_else(|| "0".to_string());
589    let minor = minor.unwrap_or_else(|| "0".to_string());
590    let sub = sub.unwrap_or_else(|| "0".to_string());
591    Ok(format!("{major}.{minor}.{sub}"))
592}
593
594fn handle_start(
595    event: &BytesStart<'_>,
596    depth: usize,
597    schema_version: &mut Option<String>,
598    top_level: &mut Vec<String>,
599) -> Result<(), XmlError> {
600    if depth == 1 && schema_version.is_none() {
601        *schema_version = extract_schema_version(event);
602    } else if depth == 2 {
603        if let Some(name) = attribute_value(event, b"Name")? {
604            top_level.push(name);
605        } else {
606            top_level.push(String::from_utf8_lossy(event.name().as_ref()).to_string());
607        }
608    }
609    Ok(())
610}
611
612fn extract_schema_version(event: &BytesStart<'_>) -> Option<String> {
613    let major = attribute_value(event, b"SchemaMajorVersion").ok().flatten();
614    let minor = attribute_value(event, b"SchemaMinorVersion").ok().flatten();
615    let sub = attribute_value(event, b"SchemaSubMinorVersion")
616        .ok()
617        .flatten();
618    if major.is_none() && minor.is_none() && sub.is_none() {
619        None
620    } else {
621        let major = major.unwrap_or_else(|| "0".to_string());
622        let minor = minor.unwrap_or_else(|| "0".to_string());
623        let sub = sub.unwrap_or_else(|| "0".to_string());
624        Some(format!("{major}.{minor}.{sub}"))
625    }
626}
627
628#[cfg(test)]
629mod tests {
630    use super::*;
631
632    const FIXTURE: &str = r#"
633        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="2" SchemaSubMinorVersion="3">
634            <Category Name="Root">
635                <pFeature>Gain</pFeature>
636                <pFeature>GainSelector</pFeature>
637            </Category>
638            <Integer Name="Width">
639                <Address>0x0000_0100</Address>
640                <Length>4</Length>
641                <AccessMode>RW</AccessMode>
642                <Min>16</Min>
643                <Max>4096</Max>
644                <Inc>2</Inc>
645            </Integer>
646            <Float Name="ExposureTime">
647                <Address>0x0000_0200</Address>
648                <Length>4</Length>
649                <AccessMode>RW</AccessMode>
650                <Min>10.0</Min>
651                <Max>200000.0</Max>
652                <Scale>1/1000</Scale>
653                <Offset>0.0</Offset>
654            </Float>
655            <Enumeration Name="GainSelector">
656                <Address>0x0000_0300</Address>
657                <Length>2</Length>
658                <AccessMode>RW</AccessMode>
659                <EnumEntry Name="AnalogAll" Value="0" />
660                <EnumEntry Name="DigitalAll" Value="1" />
661            </Enumeration>
662            <Integer Name="Gain">
663                <Address>0x0000_0304</Address>
664                <Length>2</Length>
665                <AccessMode>RW</AccessMode>
666                <Min>0</Min>
667                <Max>48</Max>
668                <pSelected>GainSelector</pSelected>
669                <Selected>AnalogAll</Selected>
670            </Integer>
671            <Boolean Name="GammaEnable">
672                <Address>0x0000_0400</Address>
673                <Length>1</Length>
674                <AccessMode>RW</AccessMode>
675            </Boolean>
676            <Command Name="AcquisitionStart">
677                <Address>0x0000_0500</Address>
678                <Length>4</Length>
679            </Command>
680        </RegisterDescription>
681    "#;
682
683    #[test]
684    fn parse_minimal_xml() {
685        let info = parse_into_minimal_nodes(FIXTURE).expect("parse xml");
686        assert_eq!(info.schema_version.as_deref(), Some("1.2.3"));
687        assert_eq!(info.top_level_features.len(), 7);
688        assert_eq!(info.top_level_features[0], "Root");
689    }
690
691    #[test]
692    fn parse_fixture_model() {
693        let model = parse(FIXTURE).expect("parse fixture");
694        assert_eq!(model.version, "1.2.3");
695        assert_eq!(model.nodes.len(), 7);
696        match &model.nodes[0] {
697            NodeDecl::Category { name, children, .. } => {
698                assert_eq!(name, "Root");
699                assert_eq!(
700                    children,
701                    &vec!["Gain".to_string(), "GainSelector".to_string()]
702                );
703            }
704            other => panic!("unexpected node: {other:?}"),
705        }
706        match &model.nodes[1] {
707            NodeDecl::Integer {
708                name,
709                min,
710                max,
711                inc,
712                ..
713            } => {
714                assert_eq!(name, "Width");
715                assert_eq!(*min, 16);
716                assert_eq!(*max, 4096);
717                assert_eq!(*inc, Some(2));
718            }
719            other => panic!("unexpected node: {other:?}"),
720        }
721        match &model.nodes[2] {
722            NodeDecl::Float {
723                name,
724                scale,
725                offset,
726                ..
727            } => {
728                assert_eq!(name, "ExposureTime");
729                assert_eq!(*scale, Some((1, 1000)));
730                assert_eq!(*offset, Some(0.0));
731            }
732            other => panic!("unexpected node: {other:?}"),
733        }
734        match &model.nodes[3] {
735            NodeDecl::Enum { name, entries, .. } => {
736                assert_eq!(name, "GainSelector");
737                assert_eq!(entries.len(), 2);
738                assert!(matches!(entries[0].value, EnumValueSrc::Literal(0)));
739                assert!(matches!(entries[1].value, EnumValueSrc::Literal(1)));
740            }
741            other => panic!("unexpected node: {other:?}"),
742        }
743        match &model.nodes[4] {
744            NodeDecl::Integer {
745                name, selected_if, ..
746            } => {
747                assert_eq!(name, "Gain");
748                assert_eq!(selected_if.len(), 1);
749                assert_eq!(selected_if[0].0, "GainSelector");
750                assert_eq!(selected_if[0].1, vec!["AnalogAll".to_string()]);
751            }
752            other => panic!("unexpected node: {other:?}"),
753        }
754    }
755
756    #[test]
757    fn parse_swissknife_node() {
758        const XML: &str = r#"
759            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
760                <Integer Name="GainRaw">
761                    <Address>0x3000</Address>
762                    <Length>4</Length>
763                    <AccessMode>RW</AccessMode>
764                    <Min>0</Min>
765                    <Max>1000</Max>
766                </Integer>
767                <Float Name="Offset">
768                    <Address>0x3008</Address>
769                    <Length>4</Length>
770                    <AccessMode>RW</AccessMode>
771                    <Min>-100.0</Min>
772                    <Max>100.0</Max>
773                </Float>
774                <SwissKnife Name="ComputedGain">
775                    <Expression>(GainRaw * 0.5) + Offset</Expression>
776                    <pVariable Name="GainRaw">GainRaw</pVariable>
777                    <pVariable Name="Offset">Offset</pVariable>
778                    <Output>Float</Output>
779                </SwissKnife>
780            </RegisterDescription>
781        "#;
782
783        let model = parse(XML).expect("parse swissknife xml");
784        assert_eq!(model.nodes.len(), 3);
785        let swiss = model
786            .nodes
787            .iter()
788            .find_map(|decl| match decl {
789                NodeDecl::SwissKnife(node) => Some(node),
790                _ => None,
791            })
792            .expect("swissknife present");
793        assert_eq!(swiss.name, "ComputedGain");
794        assert_eq!(swiss.expr, "(GainRaw * 0.5) + Offset");
795        assert_eq!(swiss.output, SkOutput::Float);
796        assert_eq!(swiss.variables.len(), 2);
797        assert_eq!(
798            swiss.variables[0],
799            ("GainRaw".to_string(), "GainRaw".to_string())
800        );
801        assert_eq!(
802            swiss.variables[1],
803            ("Offset".to_string(), "Offset".to_string())
804        );
805    }
806
807    #[test]
808    fn parse_int_swissknife_with_hex_and_ampersand() {
809        // Test that &amp; is decoded to & and hex literals are supported.
810        const XML: &str = r#"
811            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
812                <IntSwissKnife Name="PayloadSize">
813                    <pVariable Name="W">Width</pVariable>
814                    <pVariable Name="H">Height</pVariable>
815                    <pVariable Name="PF">PixelFormat</pVariable>
816                    <Formula>W * H * ((PF>>16)&amp;0xFF) / 8</Formula>
817                </IntSwissKnife>
818            </RegisterDescription>
819        "#;
820
821        let model = parse(XML).expect("parse intswissknife");
822        assert_eq!(model.nodes.len(), 1);
823        let swiss = model
824            .nodes
825            .iter()
826            .find_map(|decl| match decl {
827                NodeDecl::SwissKnife(node) => Some(node),
828                _ => None,
829            })
830            .expect("swissknife present");
831        assert_eq!(swiss.name, "PayloadSize");
832        // &amp; should be decoded to &
833        assert!(
834            swiss.expr.contains('&'),
835            "expression should contain decoded '&': {}",
836            swiss.expr
837        );
838        assert!(
839            swiss.expr.contains("0xFF"),
840            "expression should contain hex literal: {}",
841            swiss.expr
842        );
843    }
844
845    #[test]
846    fn parse_enum_entry_with_pvalue() {
847        const XML: &str = r#"
848            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
849                <Enumeration Name="Mode">
850                    <Address>0x0000_4000</Address>
851                    <Length>4</Length>
852                    <AccessMode>RW</AccessMode>
853                    <EnumEntry Name="Fixed10">
854                        <Value>10</Value>
855                    </EnumEntry>
856                    <EnumEntry Name="DynFromReg">
857                        <pValue>RegModeVal</pValue>
858                    </EnumEntry>
859                </Enumeration>
860                <Integer Name="RegModeVal">
861                    <Address>0x0000_4100</Address>
862                    <Length>4</Length>
863                    <AccessMode>RW</AccessMode>
864                    <Min>0</Min>
865                    <Max>65535</Max>
866                </Integer>
867            </RegisterDescription>
868        "#;
869
870        let model = parse(XML).expect("parse enum pvalue");
871        assert_eq!(model.nodes.len(), 2);
872        match &model.nodes[0] {
873            NodeDecl::Enum { entries, .. } => {
874                assert_eq!(entries.len(), 2);
875                assert!(matches!(entries[0].value, EnumValueSrc::Literal(10)));
876                match &entries[1].value {
877                    EnumValueSrc::FromNode(node) => assert_eq!(node, "RegModeVal"),
878                    other => panic!("unexpected entry value: {other:?}"),
879                }
880            }
881            other => panic!("unexpected node: {other:?}"),
882        }
883    }
884
885    #[test]
886    fn parse_indirect_addressing() {
887        const XML: &str = r#"
888            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
889                <Integer Name="RegAddr">
890                    <Address>0x2000</Address>
891                    <Length>4</Length>
892                    <AccessMode>RW</AccessMode>
893                    <Min>0</Min>
894                    <Max>65535</Max>
895                </Integer>
896                <Integer Name="Gain" Address="0xFFFF">
897                    <pAddress>RegAddr</pAddress>
898                    <Length>4</Length>
899                    <AccessMode>RW</AccessMode>
900                    <Min>0</Min>
901                    <Max>255</Max>
902                </Integer>
903            </RegisterDescription>
904        "#;
905
906        let model = parse(XML).expect("parse indirect xml");
907        assert_eq!(model.nodes.len(), 2);
908        match &model.nodes[0] {
909            NodeDecl::Integer {
910                name, addressing, ..
911            } => {
912                assert_eq!(name, "RegAddr");
913                assert!(
914                    matches!(addressing, Some(Addressing::Fixed { address, len }) if *address == 0x2000 && *len == 4)
915                );
916            }
917            other => panic!("unexpected node: {other:?}"),
918        }
919        match &model.nodes[1] {
920            NodeDecl::Integer {
921                name, addressing, ..
922            } => {
923                assert_eq!(name, "Gain");
924                match addressing {
925                    Some(Addressing::Indirect {
926                        p_address_node,
927                        len,
928                    }) => {
929                        assert_eq!(p_address_node, "RegAddr");
930                        assert_eq!(*len, 4);
931                    }
932                    other => panic!("expected indirect addressing, got {other:?}"),
933                }
934            }
935            other => panic!("unexpected node: {other:?}"),
936        }
937    }
938
939    #[test]
940    fn parse_integer_bitfield_big_endian() {
941        const XML: &str = r#"
942            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
943                <Integer Name="Packed">
944                    <Address>0x1000</Address>
945                    <Length>4</Length>
946                    <AccessMode>RW</AccessMode>
947                    <Min>0</Min>
948                    <Max>65535</Max>
949                    <Lsb>8</Lsb>
950                    <Msb>15</Msb>
951                    <Endianness>BigEndian</Endianness>
952                </Integer>
953            </RegisterDescription>
954        "#;
955
956        let model = parse(XML).expect("parse big-endian bitfield");
957        assert_eq!(model.nodes.len(), 1);
958        match &model.nodes[0] {
959            NodeDecl::Integer { len, bitfield, .. } => {
960                assert_eq!(*len, 4);
961                let field = bitfield.as_ref().expect("bitfield present");
962                assert_eq!(field.byte_order, ByteOrder::Big);
963                assert_eq!(field.bit_length, 8);
964                assert_eq!(field.bit_offset, 16);
965            }
966            other => panic!("unexpected node: {other:?}"),
967        }
968    }
969
970    #[test]
971    fn parse_boolean_bitfield_default_length() {
972        const XML: &str = r#"
973            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
974                <Boolean Name="Flag">
975                    <Address>0x2000</Address>
976                    <Length>1</Length>
977                    <AccessMode>RW</AccessMode>
978                    <Bit>3</Bit>
979                </Boolean>
980            </RegisterDescription>
981        "#;
982
983        let model = parse(XML).expect("parse boolean bitfield");
984        assert_eq!(model.nodes.len(), 1);
985        match &model.nodes[0] {
986            NodeDecl::Boolean { len, bitfield, .. } => {
987                assert_eq!(*len, 1);
988                let bf = bitfield.as_ref().expect("bitfield present");
989                assert_eq!(bf.byte_order, ByteOrder::Little);
990                assert_eq!(bf.bit_length, 1);
991                assert_eq!(bf.bit_offset, 3);
992            }
993            other => panic!("unexpected node: {other:?}"),
994        }
995    }
996
997    #[test]
998    fn parse_integer_bitfield_mask() {
999        const XML: &str = r#"
1000            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1001                <Integer Name="Masked">
1002                    <Address>0x3000</Address>
1003                    <Length>4</Length>
1004                    <AccessMode>RW</AccessMode>
1005                    <Min>0</Min>
1006                    <Max>65535</Max>
1007                    <Mask>0x0000FF00</Mask>
1008                </Integer>
1009            </RegisterDescription>
1010        "#;
1011
1012        let model = parse(XML).expect("parse mask bitfield");
1013        assert_eq!(model.nodes.len(), 1);
1014        match &model.nodes[0] {
1015            NodeDecl::Integer { bitfield, .. } => {
1016                let field = bitfield.as_ref().expect("bitfield present");
1017                assert_eq!(field.byte_order, ByteOrder::Little);
1018                assert_eq!(field.bit_length, 8);
1019                assert_eq!(field.bit_offset, 8);
1020            }
1021            other => panic!("unexpected node: {other:?}"),
1022        }
1023    }
1024
1025    #[test]
1026    fn parse_node_metadata() {
1027        const XML: &str = r#"
1028            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1029                <Integer Name="Width">
1030                    <Address>0x100</Address>
1031                    <Length>4</Length>
1032                    <AccessMode>RW</AccessMode>
1033                    <Min>16</Min>
1034                    <Max>4096</Max>
1035                    <Visibility>Expert</Visibility>
1036                    <Description>Image width in pixels.</Description>
1037                    <ToolTip>Width of the acquired image</ToolTip>
1038                    <DisplayName>Image Width</DisplayName>
1039                    <Representation>Linear</Representation>
1040                </Integer>
1041                <Float Name="Gain">
1042                    <Address>0x200</Address>
1043                    <Length>4</Length>
1044                    <AccessMode>RW</AccessMode>
1045                    <Min>0.0</Min>
1046                    <Max>48.0</Max>
1047                    <Unit>dB</Unit>
1048                    <Visibility>Beginner</Visibility>
1049                    <Representation>Logarithmic</Representation>
1050                </Float>
1051                <Category Name="Root">
1052                    <Visibility>Guru</Visibility>
1053                    <Description>Top-level category</Description>
1054                    <pFeature>Width</pFeature>
1055                    <pFeature>Gain</pFeature>
1056                </Category>
1057                <Enumeration Name="PixelFormat">
1058                    <Address>0x300</Address>
1059                    <Length>4</Length>
1060                    <AccessMode>RW</AccessMode>
1061                    <Visibility>Beginner</Visibility>
1062                    <ToolTip>Pixel format selector</ToolTip>
1063                    <EnumEntry Name="Mono8" Value="0" />
1064                </Enumeration>
1065            </RegisterDescription>
1066        "#;
1067
1068        let model = parse(XML).expect("parse metadata xml");
1069        assert_eq!(model.nodes.len(), 4);
1070
1071        // Integer with full metadata
1072        match &model.nodes[0] {
1073            NodeDecl::Integer { name, meta, .. } => {
1074                assert_eq!(name, "Width");
1075                assert_eq!(meta.visibility, Visibility::Expert);
1076                assert_eq!(meta.description.as_deref(), Some("Image width in pixels."));
1077                assert_eq!(meta.tooltip.as_deref(), Some("Width of the acquired image"));
1078                assert_eq!(meta.display_name.as_deref(), Some("Image Width"));
1079                assert_eq!(meta.representation, Some(Representation::Linear));
1080            }
1081            other => panic!("unexpected node: {other:?}"),
1082        }
1083
1084        // Float with visibility + representation
1085        match &model.nodes[1] {
1086            NodeDecl::Float { name, meta, .. } => {
1087                assert_eq!(name, "Gain");
1088                assert_eq!(meta.visibility, Visibility::Beginner);
1089                assert_eq!(meta.representation, Some(Representation::Logarithmic));
1090                assert!(meta.description.is_none());
1091            }
1092            other => panic!("unexpected node: {other:?}"),
1093        }
1094
1095        // Category with visibility + description
1096        match &model.nodes[2] {
1097            NodeDecl::Category { name, meta, .. } => {
1098                assert_eq!(name, "Root");
1099                assert_eq!(meta.visibility, Visibility::Guru);
1100                assert_eq!(meta.description.as_deref(), Some("Top-level category"));
1101            }
1102            other => panic!("unexpected node: {other:?}"),
1103        }
1104
1105        // Enum with visibility + tooltip
1106        match &model.nodes[3] {
1107            NodeDecl::Enum { name, meta, .. } => {
1108                assert_eq!(name, "PixelFormat");
1109                assert_eq!(meta.visibility, Visibility::Beginner);
1110                assert_eq!(meta.tooltip.as_deref(), Some("Pixel format selector"));
1111            }
1112            other => panic!("unexpected node: {other:?}"),
1113        }
1114    }
1115}