Skip to main content

xbrl_rs/instance/
parser.rs

1use crate::{
2    NamespacePrefix, NamespaceUri, QName, XbrlError,
3    xml::{self, ArcroleRef, RoleRef, SchemaRef, parse_qname},
4};
5use quick_xml::{
6    Reader,
7    events::{BytesStart, Event, attributes::Attributes},
8};
9use std::{
10    collections::HashMap,
11    fs::File,
12    io::{BufRead, BufReader},
13    path::{Path, PathBuf},
14};
15
16/// An `xbrli:context` element as parsed from the instance document.
17#[derive(Debug, PartialEq, Eq)]
18pub struct RawContext {
19    /// Id attribute of the context.
20    pub id: String,
21    /// Entity definition for the context.
22    pub entity: RawEntity,
23    /// Period definition for the context.
24    pub period: RawPeriod,
25    /// Scenario dimensions for the context.
26    pub scenario_dimensions: Vec<RawDimension>,
27}
28
29/// An `xbrli:entity` element as parsed from the instance document.
30#[derive(Debug, PartialEq, Eq)]
31pub struct RawEntity {
32    /// Identifier for the entity, typically a legal entity identifier (LEI).
33    pub identifier: String,
34    /// Scheme for the entity identifier, typically a URI that defines the
35    /// syntax and semantics of the identifier (e.g.
36    /// "http://standards.iso.org/iso/17442" for LEIs).
37    pub scheme: String,
38    /// Segment dimensions for the entity.
39    pub segment_dimensions: Vec<RawDimension>,
40}
41
42/// An `xbrli:period` element as parsed from the instance document.
43#[derive(Debug, PartialEq, Eq)]
44pub enum RawPeriod {
45    Instant(String),
46    Duration {
47        start_date: String,
48        end_date: String,
49    },
50    Forever,
51}
52
53/// A dimension defined in a `scenario` or `segment` element.
54#[derive(Debug, PartialEq, Eq)]
55pub struct RawDimension {
56    /// QName of the dimension
57    pub dimension: QName,
58    /// QName of the member
59    pub member: QName,
60}
61
62#[derive(Debug, PartialEq, Eq)]
63pub struct RawUnit {
64    /// Unique ID of the unit as specified in the instance document.
65    pub id: String,
66    /// For a simple unit, this will be the only measure. For a divide unit,
67    /// this is the numerator.
68    pub numerator: Vec<QName>,
69    /// For a simple unit, this will be empty. For a divide unit, this is the
70    /// denominator.
71    pub denominator: Vec<QName>,
72}
73
74/// A fact in the instance document, which can be either an item or a tuple.
75#[derive(Debug, PartialEq, Eq)]
76pub enum RawFact {
77    Item(RawItemFact),
78    Tuple(RawTupleFact),
79}
80
81#[derive(Debug, PartialEq, Eq)]
82pub struct RawItemFact {
83    /// QName of the corresponding concept
84    pub name: QName,
85    /// Raw text value
86    pub value: String,
87    /// contextRef attribute
88    pub context_ref: String,
89    /// unitRef attribute
90    pub unit_ref: Option<String>,
91    /// decimals attribute
92    pub decimals: Option<String>,
93    /// precision attribute
94    pub precision: Option<String>,
95    /// id attribute
96    pub id: Option<String>,
97    /// xsi:nil attribute
98    pub is_nil: bool,
99}
100
101#[derive(Debug, PartialEq, Eq)]
102pub struct RawTupleFact {
103    /// QName of the corresponding concept
104    pub name: QName,
105    /// id attribute
106    pub id: Option<String>,
107    /// xsi:nil attribute
108    pub is_nil: bool,
109    /// Child facts (items or nested tuples)
110    pub children: Vec<RawFact>,
111}
112
113/// A locator in a footnote link, usually a `link:loc` element.
114#[derive(Debug, PartialEq, Eq)]
115pub struct Locator {
116    /// Local name of the locator element (e.g. `loc` or a custom element).
117    pub label: String,
118    /// Optional `xlink:href` target, typically a same-document fragment.
119    pub href: String,
120}
121
122#[derive(Debug, PartialEq, Eq)]
123pub struct RawFootnoteLink {
124    pub role: String,
125    pub locators: Vec<Locator>,
126    pub arcs: Vec<FootnoteArc>,
127    pub footnotes: Vec<FootnoteResource>,
128}
129
130#[derive(Debug, PartialEq, Eq)]
131pub struct FootnoteArc {
132    pub from: String,
133    pub to: String,
134}
135
136#[derive(Debug, PartialEq, Eq)]
137pub struct FootnoteResource {
138    pub label: String,
139    pub lang: Option<String>,
140    pub text: String,
141}
142
143#[derive(Debug, PartialEq, Eq, Default)]
144pub struct RawInstance {
145    /// Namespace declarations (prefix -> URI)
146    pub namespaces: HashMap<NamespacePrefix, NamespaceUri>,
147    /// Schema references
148    pub schema_refs: Vec<SchemaRef>,
149    /// Role references
150    ///
151    /// Usually defined in the linkbase document, but can also be present in the
152    /// instance document.
153    pub role_refs: Vec<RoleRef>,
154    /// Arcrole references
155    ///
156    /// Usually defined in the linkbase document, but can also be present in the
157    /// instance document.
158    pub arcrole_refs: Vec<ArcroleRef>,
159    /// Context definitions
160    pub contexts: Vec<RawContext>,
161    /// Unit definitions
162    pub units: Vec<RawUnit>,
163    /// All facts
164    pub facts: Vec<RawFact>,
165    /// Optional footnote links
166    pub footnote_links: Vec<RawFootnoteLink>,
167}
168
169impl RawInstance {
170    #[allow(clippy::too_many_arguments)]
171    pub fn new(
172        namespaces: HashMap<NamespacePrefix, NamespaceUri>,
173        schema_refs: Vec<SchemaRef>,
174        role_refs: Vec<RoleRef>,
175        arcrole_refs: Vec<ArcroleRef>,
176        contexts: Vec<RawContext>,
177        units: Vec<RawUnit>,
178        facts: Vec<RawFact>,
179        footnote_links: Vec<RawFootnoteLink>,
180    ) -> Self {
181        Self {
182            namespaces,
183            schema_refs,
184            role_refs,
185            arcrole_refs,
186            contexts,
187            units,
188            facts,
189            footnote_links,
190        }
191    }
192}
193
194/// The parser for XBRL instance documents.
195pub struct InstanceParser<R> {
196    /// Path of the currently parsed instance file if available. Used for error
197    /// reporting.
198    path: Option<PathBuf>,
199    /// The XML reader for the instance document.
200    reader: Reader<R>,
201}
202
203impl InstanceParser<BufReader<File>> {
204    /// Creates a new `InstanceParser` from the given file path.
205    pub fn from_file(path: &Path) -> Result<Self, XbrlError> {
206        let file = File::open(path).map_err(|err| XbrlError::FileOpen {
207            path: path.to_path_buf(),
208            context: "opening file".to_string(),
209            source: err,
210        })?;
211        let reader = Reader::from_reader(BufReader::new(file));
212
213        Ok(Self {
214            path: Some(path.to_path_buf()),
215            reader,
216        })
217    }
218}
219
220impl<R: BufRead> InstanceParser<R> {
221    /// Creates a new `InstanceParser` with the given reader and file path.
222    pub fn new(reader: Reader<R>) -> Self {
223        Self { path: None, reader }
224    }
225
226    /// Creates a new `InstanceParser` from the given reader.
227    pub fn from_reader(reader: R) -> Self {
228        let mut reader = Reader::from_reader(reader);
229        reader.config_mut().trim_text_start = true;
230        reader.config_mut().trim_text_end = true;
231        Self::new(reader)
232    }
233
234    /// Parses an XBRL instance document from the reader. Path is used for error
235    /// reporting.
236    pub fn parse_instance(&mut self) -> Result<RawInstance, XbrlError> {
237        let mut instance = RawInstance::default();
238        let mut has_instance_root = false;
239        let mut buf = Vec::new();
240
241        loop {
242            match self.reader.read_event_into(&mut buf) {
243                Ok(Event::Start(ref event)) => {
244                    let event_name = event.name();
245                    let local_name = event_name.local_name();
246                    let attributes = event.attributes();
247
248                    match local_name.as_ref() {
249                        b"xbrl" => {
250                            has_instance_root = true;
251                            self.parse_instance_root(&mut instance, attributes)?;
252                        }
253                        b"schemaRef" => self.parse_schema_ref(&mut instance, attributes)?,
254                        b"roleRef" => self.parse_role_ref(&mut instance, attributes)?,
255                        b"arcroleRef" => self.parse_arcrole_ref(&mut instance, attributes)?,
256                        b"context" => self.parse_context(&mut instance, attributes)?,
257                        b"unit" => self.parse_unit(&mut instance, attributes)?,
258                        b"footnoteLink" => self.parse_footnote_link(&mut instance, attributes)?,
259                        _ if Self::is_fact_element(local_name.as_ref()) => {
260                            self.parse_fact(&mut instance, event)?;
261                        }
262                        _ => {}
263                    }
264                }
265                Ok(Event::Empty(ref event)) => {
266                    let local_name = event.name().local_name();
267                    let attributes = event.attributes();
268
269                    match local_name.as_ref() {
270                        b"xbrl" => {
271                            has_instance_root = true;
272                            self.parse_instance_root(&mut instance, attributes)?;
273                        }
274                        b"schemaRef" => self.parse_schema_ref(&mut instance, attributes)?,
275                        b"roleRef" => self.parse_role_ref(&mut instance, attributes)?,
276                        b"arcroleRef" => self.parse_arcrole_ref(&mut instance, attributes)?,
277                        _ if Self::is_fact_element(local_name.as_ref()) => {
278                            let fact = self.parse_empty_fact(event)?;
279                            instance.facts.push(fact);
280                        }
281                        _ => {}
282                    }
283                }
284                Ok(Event::End(ref event)) if event.name().local_name().as_ref() == b"xbrl" => {
285                    break;
286                }
287                Ok(Event::End(_)) => {}
288                Ok(Event::Text(_)) => {}
289                Ok(Event::Eof) => break,
290                Err(err) => {
291                    return Err(XbrlError::XmlParse {
292                        path: self.path.clone(),
293                        position: self.reader.buffer_position(),
294                        element: Some("schema".to_string()),
295                        source: err,
296                    });
297                }
298                _ => {}
299            }
300        }
301
302        if !has_instance_root {
303            return Err(XbrlError::InvalidInstanceDocument {
304                path: self.path.clone(),
305                reason: "missing <xbrli:xbrl> root element".to_string(),
306            });
307        }
308
309        Ok(instance)
310    }
311
312    /// Parses the root <xbrli:xbrl> element to extract namespace declarations.
313    fn parse_instance_root(
314        &mut self,
315        instance: &mut RawInstance,
316        attributes: Attributes,
317    ) -> Result<(), XbrlError> {
318        for attribute in attributes {
319            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
320                path: self.path.clone(),
321                position: self.reader.buffer_position(),
322                element: Some("xbrl".to_string()),
323                source: err.into(),
324            })?;
325            let key = attribute.key;
326
327            if let Some(prefix) = key.prefix()
328                && prefix.as_ref() == b"xmlns"
329            {
330                let local = key.local_name();
331                let namespace_prefix = str::from_utf8(local.as_ref())?;
332                let uri = attribute.decode_and_unescape_value(self.reader.decoder())?;
333                instance.namespaces.insert(
334                    NamespacePrefix::from(namespace_prefix),
335                    NamespaceUri::from(uri.into_owned()),
336                );
337            }
338        }
339
340        Ok(())
341    }
342
343    /// Parse the `link:schemaRef` element to extract the schema reference.
344    fn parse_schema_ref(
345        &mut self,
346        instance: &mut RawInstance,
347        attributes: Attributes,
348    ) -> Result<(), XbrlError> {
349        for attribute in attributes {
350            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
351                path: self.path.clone(),
352                position: self.reader.buffer_position(),
353                element: Some("schemaRef".to_string()),
354                source: err.into(),
355            })?;
356
357            if attribute.key.local_name().as_ref() == b"href" {
358                let value = attribute.decode_and_unescape_value(self.reader.decoder())?;
359                instance.schema_refs.push(SchemaRef {
360                    href: value.into_owned(),
361                });
362                return Ok(());
363            }
364        }
365
366        Err(XbrlError::InvalidInstanceDocument {
367            path: self.path.clone(),
368            reason: "missing xlink:href in link:schemaRef".to_string(),
369        })
370    }
371
372    /// Parse the `link:roleRef` element to extract the role reference.
373    fn parse_role_ref(
374        &mut self,
375        instance: &mut RawInstance,
376        attributes: Attributes,
377    ) -> Result<(), XbrlError> {
378        let mut role_uri = None;
379        let mut href = None;
380
381        for attribute in attributes {
382            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
383                path: self.path.clone(),
384                position: self.reader.buffer_position(),
385                element: Some("roleRef".to_string()),
386                source: err.into(),
387            })?;
388            let local_name = attribute.key.local_name();
389            let value = attribute.decode_and_unescape_value(self.reader.decoder())?;
390
391            match local_name.as_ref() {
392                b"roleURI" => role_uri = Some(value.into_owned()),
393                b"href" => href = Some(value.into_owned()),
394                _ => {}
395            }
396        }
397
398        instance.role_refs.push(RoleRef {
399            role_uri: role_uri.ok_or_else(|| XbrlError::InvalidInstanceDocument {
400                path: self.path.clone(),
401                reason: "missing roleURI in link:roleRef".to_string(),
402            })?,
403            href: href.ok_or_else(|| XbrlError::InvalidInstanceDocument {
404                path: self.path.clone(),
405                reason: "missing xlink:href in link:roleRef".to_string(),
406            })?,
407        });
408
409        Ok(())
410    }
411
412    /// Parse the `link:arcroleRef` element to extract the arcrole reference.
413    fn parse_arcrole_ref(
414        &mut self,
415        instance: &mut RawInstance,
416        attributes: Attributes,
417    ) -> Result<(), XbrlError> {
418        let mut arcrole_uri = None;
419        let mut href = None;
420
421        for attribute in attributes {
422            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
423                path: self.path.clone(),
424                position: self.reader.buffer_position(),
425                element: Some("arcroleRef".to_string()),
426                source: err.into(),
427            })?;
428            let local_name = attribute.key.local_name();
429            let value = attribute.decode_and_unescape_value(self.reader.decoder())?;
430
431            match local_name.as_ref() {
432                b"arcroleURI" => arcrole_uri = Some(value.into_owned()),
433                b"href" => href = Some(value.into_owned()),
434                _ => {}
435            }
436        }
437
438        instance.arcrole_refs.push(ArcroleRef {
439            arcrole_uri: arcrole_uri.ok_or_else(|| XbrlError::InvalidInstanceDocument {
440                path: self.path.clone(),
441                reason: "missing arcroleURI in link:arcroleRef".to_string(),
442            })?,
443            href: href.ok_or_else(|| XbrlError::InvalidInstanceDocument {
444                path: self.path.clone(),
445                reason: "missing xlink:href in link:arcroleRef".to_string(),
446            })?,
447        });
448
449        Ok(())
450    }
451
452    /// Parse the `xbrli:context` element to extract the context definition.
453    ///
454    /// `xbrli:segment` and `xbrli:scenario` elements are parsed as dimensional
455    /// containers. `xbrli:segment` is always a child of `xbrli:entity`, while
456    /// `xbrli:scenario` is a direct child of `xbrli:context`.
457    fn parse_context(
458        &mut self,
459        instance: &mut RawInstance,
460        attributes: Attributes,
461    ) -> Result<(), XbrlError> {
462        let mut id = None;
463
464        for attribute in attributes {
465            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
466                path: self.path.clone(),
467                position: self.reader.buffer_position(),
468                element: Some("context".to_string()),
469                source: err.into(),
470            })?;
471
472            if attribute.key.local_name().as_ref() == b"id" {
473                let value = attribute.decode_and_unescape_value(self.reader.decoder())?;
474                id = Some(value.into_owned());
475            }
476        }
477
478        let id = id.ok_or_else(|| XbrlError::InvalidInstanceDocument {
479            path: self.path.clone(),
480            reason: "missing id in xbrli:context".to_string(),
481        })?;
482
483        let mut entity = None;
484        let mut period = None;
485        let mut scenario_dimensions = Vec::new();
486        let mut buf = Vec::new();
487
488        loop {
489            match self.reader.read_event_into(&mut buf)? {
490                Event::Start(ref event) => match event.local_name().as_ref() {
491                    b"entity" => {
492                        entity = Some(self.parse_entity()?);
493                    }
494                    b"period" => {
495                        period = Some(self.parse_period()?);
496                    }
497                    b"scenario" => {
498                        self.parse_dimensional_container(&mut scenario_dimensions)?;
499                    }
500                    _ => {}
501                },
502                Event::End(ref event) if event.local_name().as_ref() == b"context" => break,
503                Event::Eof => break,
504                _ => {}
505            }
506            buf.clear();
507        }
508
509        instance.contexts.push(RawContext {
510            id,
511            entity: entity.ok_or_else(|| XbrlError::InvalidInstanceDocument {
512                path: self.path.clone(),
513                reason: "missing entity in xbrli:context".to_string(),
514            })?,
515            period: period.ok_or_else(|| XbrlError::InvalidInstanceDocument {
516                path: self.path.clone(),
517                reason: "missing period in xbrli:context".to_string(),
518            })?,
519            scenario_dimensions,
520        });
521
522        Ok(())
523    }
524
525    /// Parse the `xbrli:entity` element to extract the entity identifier and
526    /// scheme.
527    fn parse_entity(&mut self) -> Result<RawEntity, XbrlError> {
528        let mut identifier = None;
529        let mut scheme = None;
530        let mut segment_dimensions = Vec::new();
531        let mut buf = Vec::new();
532
533        loop {
534            match self.reader.read_event_into(&mut buf)? {
535                Event::Start(ref event) => match event.local_name().as_ref() {
536                    b"identifier" => {
537                        for attribute in event.attributes() {
538                            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
539                                path: self.path.clone(),
540                                position: self.reader.buffer_position(),
541                                element: Some("identifier".to_string()),
542                                source: err.into(),
543                            })?;
544
545                            if attribute.key.local_name().as_ref() == b"scheme" {
546                                let value =
547                                    attribute.decode_and_unescape_value(self.reader.decoder())?;
548                                scheme = Some(value.into_owned());
549                            }
550                        }
551                    }
552                    b"segment" => {
553                        self.parse_dimensional_container(&mut segment_dimensions)?;
554                    }
555                    _ => {}
556                },
557                Event::Text(ref text) => {
558                    if identifier.is_none() {
559                        let value = text.xml_content().map_err(quick_xml::Error::from)?;
560                        identifier = Some(value.into_owned());
561                    }
562                }
563                Event::End(ref event) if event.local_name().as_ref() == b"entity" => break,
564                Event::Eof => break,
565                _ => {}
566            }
567            buf.clear();
568        }
569
570        Ok(RawEntity {
571            identifier: identifier.ok_or_else(|| XbrlError::InvalidInstanceDocument {
572                path: self.path.clone(),
573                reason: "missing identifier in xbrli:entity".to_string(),
574            })?,
575            scheme: scheme.ok_or_else(|| XbrlError::InvalidInstanceDocument {
576                path: self.path.clone(),
577                reason: "missing scheme in xbrli:identifier".to_string(),
578            })?,
579            segment_dimensions,
580        })
581    }
582
583    /// Parse the `xbrli:period` element to extract the period definition.
584    fn parse_period(&mut self) -> Result<RawPeriod, XbrlError> {
585        let mut instant = None;
586        let mut start_date = None;
587        let mut end_date = None;
588        let mut is_forever = false;
589        let mut current_tag: Option<String> = None;
590        let mut buf = Vec::new();
591
592        loop {
593            match self.reader.read_event_into(&mut buf)? {
594                Event::Start(ref event) | Event::Empty(ref event) => {
595                    match event.local_name().as_ref() {
596                        b"instant" => current_tag = Some("instant".to_string()),
597                        b"startDate" => current_tag = Some("startDate".to_string()),
598                        b"endDate" => current_tag = Some("endDate".to_string()),
599                        b"forever" => is_forever = true,
600                        _ => {}
601                    }
602                }
603                Event::Text(ref text) => {
604                    let value = text
605                        .xml_content()
606                        .map_err(quick_xml::Error::from)?
607                        .into_owned();
608                    match current_tag.as_deref() {
609                        Some("instant") => instant = Some(value),
610                        Some("startDate") => start_date = Some(value),
611                        Some("endDate") => end_date = Some(value),
612                        _ => {}
613                    }
614                }
615                Event::End(ref event) => match event.local_name().as_ref() {
616                    b"period" => break,
617                    _ => current_tag = None,
618                },
619                Event::Eof => break,
620                _ => {}
621            }
622            buf.clear();
623        }
624
625        if is_forever {
626            Ok(RawPeriod::Forever)
627        } else if let Some(instant) = instant {
628            Ok(RawPeriod::Instant(instant))
629        } else if let (Some(start_date), Some(end_date)) = (start_date, end_date) {
630            Ok(RawPeriod::Duration {
631                start_date,
632                end_date,
633            })
634        } else {
635            Err(XbrlError::InvalidInstanceDocument {
636                path: self.path.clone(),
637                reason: "invalid period in xbrli:context".to_string(),
638            })
639        }
640    }
641
642    /// Parse the dimensions defined in a `scenario` or `segment` element.
643    fn parse_dimensional_container(
644        &mut self,
645        dimensions: &mut Vec<RawDimension>,
646    ) -> Result<(), XbrlError> {
647        let mut buf = Vec::new();
648
649        loop {
650            match self.reader.read_event_into(&mut buf)? {
651                Event::Start(ref event) | Event::Empty(ref event) => {
652                    if event.local_name().as_ref() == b"explicitMember" {
653                        let mut dimension = None;
654
655                        for attribute in event.attributes() {
656                            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
657                                path: self.path.clone(),
658                                position: self.reader.buffer_position(),
659                                element: Some("explicitMember".to_string()),
660                                source: err.into(),
661                            })?;
662
663                            if attribute.key.local_name().as_ref() == b"dimension" {
664                                let value =
665                                    attribute.decode_and_unescape_value(self.reader.decoder())?;
666                                dimension = Some(parse_qname(&value));
667                            }
668                        }
669
670                        if let Some(dimension) = dimension {
671                            let mut member_buf = Vec::new();
672
673                            if let Event::Text(ref text) =
674                                self.reader.read_event_into(&mut member_buf)?
675                            {
676                                let member = text
677                                    .xml_content()
678                                    .map_err(quick_xml::Error::from)?
679                                    .into_owned();
680                                let member = parse_qname(&member);
681                                dimensions.push(RawDimension { dimension, member });
682                            }
683                        }
684                    }
685                }
686                Event::End(ref event)
687                    if matches!(event.local_name().as_ref(), b"scenario" | b"segment") =>
688                {
689                    break;
690                }
691                Event::Eof => break,
692                _ => {}
693            }
694            buf.clear();
695        }
696
697        Ok(())
698    }
699
700    /// Parse the `xbrli:unit` element to extract the unit definition, including
701    /// measures and divide units.
702    fn parse_unit(
703        &mut self,
704        instance: &mut RawInstance,
705        attributes: Attributes,
706    ) -> Result<(), XbrlError> {
707        let mut id = None;
708
709        for attribute in attributes {
710            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
711                path: self.path.clone(),
712                position: self.reader.buffer_position(),
713                element: Some("unit".to_string()),
714                source: err.into(),
715            })?;
716
717            if attribute.key.local_name().as_ref() == b"id" {
718                let value = attribute.decode_and_unescape_value(self.reader.decoder())?;
719                id = Some(value.into_owned());
720            }
721        }
722
723        let id = id.ok_or_else(|| XbrlError::InvalidInstanceDocument {
724            path: self.path.clone(),
725            reason: "missing id in xbrli:unit".to_string(),
726        })?;
727        let mut numerator = Vec::new();
728        let mut denominator = Vec::new();
729        let mut buf = Vec::new();
730
731        loop {
732            match self.reader.read_event_into(&mut buf)? {
733                Event::Start(ref event) => match event.local_name().as_ref() {
734                    b"measure" => {
735                        let mut text_buf = Vec::new();
736                        if let Event::Text(ref text) = self.reader.read_event_into(&mut text_buf)? {
737                            let value = text.xml_content().map_err(quick_xml::Error::from)?;
738                            let qname = xml::parse_qname(&value);
739                            numerator.push(qname);
740                        }
741                    }
742                    b"divide" => {
743                        self.parse_unit_divide(&mut numerator, &mut denominator)?;
744                    }
745                    _ => {}
746                },
747                Event::End(ref event) if event.local_name().as_ref() == b"unit" => break,
748                Event::Eof => break,
749                _ => {}
750            }
751            buf.clear();
752        }
753
754        instance.units.push(RawUnit {
755            id,
756            numerator,
757            denominator,
758        });
759
760        Ok(())
761    }
762
763    /// Parse the `divide` element inside a `unit` to extract the numerator and
764    /// denominator measures.
765    fn parse_unit_divide(
766        &mut self,
767        numerator: &mut Vec<QName>,
768        denominator: &mut Vec<QName>,
769    ) -> Result<(), XbrlError> {
770        let mut in_numerator = false;
771        let mut in_denominator = false;
772        let mut buf = Vec::new();
773
774        loop {
775            match self.reader.read_event_into(&mut buf)? {
776                Event::Start(ref event) => match event.local_name().as_ref() {
777                    b"unitNumerator" => in_numerator = true,
778                    b"unitDenominator" => in_denominator = true,
779                    b"measure" => {
780                        let mut text_buf = Vec::new();
781                        if let Event::Text(ref text) = self.reader.read_event_into(&mut text_buf)? {
782                            let value = text.xml_content().map_err(quick_xml::Error::from)?;
783                            let qname = xml::parse_qname(&value);
784                            if in_numerator {
785                                numerator.push(qname);
786                            } else if in_denominator {
787                                denominator.push(qname);
788                            }
789                        }
790                    }
791                    _ => {}
792                },
793                Event::End(ref event) => match event.local_name().as_ref() {
794                    b"unitNumerator" => in_numerator = false,
795                    b"unitDenominator" => in_denominator = false,
796                    b"divide" => break,
797                    _ => {}
798                },
799                Event::Eof => break,
800                _ => {}
801            }
802            buf.clear();
803        }
804
805        Ok(())
806    }
807
808    /// Check if a local element name represents a fact (as opposed to a
809    /// structural XBRL element like context, unit, schemaRef, etc.).
810    fn is_fact_element(local_name: &[u8]) -> bool {
811        !matches!(
812            local_name,
813            b"xbrl"
814                | b"context"
815                | b"unit"
816                | b"schemaRef"
817                | b"roleRef"
818                | b"arcroleRef"
819                | b"identifier"
820                | b"entity"
821                | b"period"
822                | b"instant"
823                | b"startDate"
824                | b"endDate"
825                | b"scenario"
826                | b"segment"
827                | b"explicitMember"
828                | b"measure"
829                | b"footnoteLink"
830                | b"footnote"
831                | b"footnoteArc"
832                | b"loc"
833                | b"forever"
834                | b"unitNumerator"
835                | b"unitDenominator"
836                | b"divide"
837        )
838    }
839
840    /// Parse a fact element (item or tuple).
841    ///
842    /// If `contextRef` is present the element is an item fact; otherwise it is
843    /// a tuple fact whose children are parsed recursively.
844    fn parse_fact(
845        &mut self,
846        instance: &mut RawInstance,
847        event: &BytesStart,
848    ) -> Result<(), XbrlError> {
849        let fact = self.parse_fact_recursive(event)?;
850
851        if let Some(fact) = fact {
852            instance.facts.push(fact);
853        }
854
855        Ok(())
856    }
857
858    /// Recursively parse a single fact element, returning `None` for
859    /// self-closing elements without `contextRef` that have no children
860    /// (empty tuples are still returned).
861    fn parse_fact_recursive(&mut self, event: &BytesStart) -> Result<Option<RawFact>, XbrlError> {
862        let name = parse_qname(std::str::from_utf8(event.name().as_ref())?);
863
864        let mut context_ref = None;
865        let mut unit_ref = None;
866        let mut decimals = None;
867        let mut precision = None;
868        let mut id = None;
869        let mut is_nil = false;
870
871        for attribute in event.attributes() {
872            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
873                path: self.path.clone(),
874                position: self.reader.buffer_position(),
875                element: Some(name.to_string()),
876                source: err.into(),
877            })?;
878            let local_name = attribute.key.local_name();
879            let value = attribute.decode_and_unescape_value(self.reader.decoder())?;
880
881            match local_name.as_ref() {
882                b"contextRef" => context_ref = Some(value.into_owned()),
883                b"unitRef" => unit_ref = Some(value.into_owned()),
884                b"decimals" => decimals = Some(value.into_owned()),
885                b"precision" => precision = Some(value.into_owned()),
886                b"id" => id = Some(value.into_owned()),
887                b"nil" => is_nil = value.as_ref() == "true",
888                _ => {}
889            }
890        }
891
892        if let Some(context_ref) = context_ref {
893            let mut value = String::new();
894            let mut buf = Vec::new();
895
896            // Item fact: read text value until closing tag
897            loop {
898                match self.reader.read_event_into(&mut buf)? {
899                    Event::Text(ref text) => {
900                        let decoded = text.xml_content().map_err(quick_xml::Error::from)?;
901                        value.push_str(&decoded);
902                    }
903                    Event::End(ref end) if end.name().as_ref() == event.name().as_ref() => break,
904                    Event::Eof => break,
905                    _ => {}
906                }
907                buf.clear();
908            }
909
910            Ok(Some(RawFact::Item(RawItemFact {
911                name,
912                value,
913                context_ref,
914                unit_ref,
915                decimals,
916                precision,
917                id,
918                is_nil,
919            })))
920        } else {
921            let mut children = Vec::new();
922            let mut buf = Vec::new();
923
924            // Tuple fact: recursively parse child facts until closing tag
925            loop {
926                match self.reader.read_event_into(&mut buf)? {
927                    Event::Start(ref child_event) => {
928                        if Self::is_fact_element(child_event.name().local_name().as_ref())
929                            && let Some(child) = self.parse_fact_recursive(child_event)?
930                        {
931                            children.push(child);
932                        }
933                    }
934                    Event::Empty(ref child_event) => {
935                        if Self::is_fact_element(child_event.name().local_name().as_ref()) {
936                            children.push(self.parse_empty_fact(child_event)?);
937                        }
938                    }
939                    Event::End(ref end) if end.name().as_ref() == event.name().as_ref() => break,
940                    Event::Eof => break,
941                    _ => {}
942                }
943                buf.clear();
944            }
945
946            Ok(Some(RawFact::Tuple(RawTupleFact {
947                name,
948                id,
949                is_nil,
950                children,
951            })))
952        }
953    }
954
955    /// Parse a self-closing (empty) fact element.
956    fn parse_empty_fact(&mut self, event: &BytesStart) -> Result<RawFact, XbrlError> {
957        let name = parse_qname(std::str::from_utf8(event.name().as_ref())?);
958
959        let mut context_ref = None;
960        let mut unit_ref = None;
961        let mut decimals = None;
962        let mut precision = None;
963        let mut id = None;
964        let mut is_nil = false;
965
966        for attribute in event.attributes() {
967            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
968                path: self.path.clone(),
969                position: self.reader.buffer_position(),
970                element: Some(name.to_string()),
971                source: err.into(),
972            })?;
973            let local_name = attribute.key.local_name();
974            let value = attribute.decode_and_unescape_value(self.reader.decoder())?;
975
976            match local_name.as_ref() {
977                b"contextRef" => context_ref = Some(value.into_owned()),
978                b"unitRef" => unit_ref = Some(value.into_owned()),
979                b"decimals" => decimals = Some(value.into_owned()),
980                b"precision" => precision = Some(value.into_owned()),
981                b"id" => id = Some(value.into_owned()),
982                b"nil" => is_nil = value.as_ref() == "true",
983                _ => {}
984            }
985        }
986
987        if let Some(context_ref) = context_ref {
988            Ok(RawFact::Item(RawItemFact {
989                name,
990                value: String::new(),
991                context_ref,
992                unit_ref,
993                decimals,
994                precision,
995                id,
996                is_nil,
997            }))
998        } else {
999            Ok(RawFact::Tuple(RawTupleFact {
1000                name,
1001                id,
1002                is_nil,
1003                children: Vec::new(),
1004            }))
1005        }
1006    }
1007
1008    /// Parse the `link:footnoteLink` element to extract the footnote link.
1009    fn parse_footnote_link(
1010        &mut self,
1011        instance: &mut RawInstance,
1012        attributes: Attributes,
1013    ) -> Result<(), XbrlError> {
1014        let mut role = String::new();
1015
1016        for attribute in attributes {
1017            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
1018                path: self.path.clone(),
1019                position: self.reader.buffer_position(),
1020                element: Some("footnoteLink".to_string()),
1021                source: err.into(),
1022            })?;
1023
1024            if attribute.key.local_name().as_ref() == b"role" {
1025                let value = attribute.decode_and_unescape_value(self.reader.decoder())?;
1026                role = value.into_owned();
1027            }
1028        }
1029
1030        let mut locators = Vec::new();
1031        let mut arcs = Vec::new();
1032        let mut footnotes = Vec::new();
1033        let mut buf = Vec::new();
1034
1035        loop {
1036            match self.reader.read_event_into(&mut buf)? {
1037                Event::Start(ref event) | Event::Empty(ref event) => {
1038                    match event.local_name().as_ref() {
1039                        b"loc" => {
1040                            let mut label = None;
1041                            let mut href = None;
1042
1043                            for attribute in event.attributes() {
1044                                let attribute = attribute.map_err(|err| XbrlError::XmlParse {
1045                                    path: self.path.clone(),
1046                                    position: self.reader.buffer_position(),
1047                                    element: Some("loc".to_string()),
1048                                    source: err.into(),
1049                                })?;
1050                                let local_name = attribute.key.local_name();
1051                                let value =
1052                                    attribute.decode_and_unescape_value(self.reader.decoder())?;
1053
1054                                match local_name.as_ref() {
1055                                    b"label" => label = Some(value.into_owned()),
1056                                    b"href" => href = Some(value.into_owned()),
1057                                    _ => {}
1058                                }
1059                            }
1060
1061                            if let (Some(label), Some(href)) = (label, href) {
1062                                locators.push(Locator { label, href });
1063                            }
1064                        }
1065                        b"footnoteArc" => {
1066                            let mut from = None;
1067                            let mut to = None;
1068
1069                            for attribute in event.attributes() {
1070                                let attribute = attribute.map_err(|err| XbrlError::XmlParse {
1071                                    path: self.path.clone(),
1072                                    position: self.reader.buffer_position(),
1073                                    element: Some("footnoteArc".to_string()),
1074                                    source: err.into(),
1075                                })?;
1076                                let local_name = attribute.key.local_name();
1077                                let value =
1078                                    attribute.decode_and_unescape_value(self.reader.decoder())?;
1079
1080                                match local_name.as_ref() {
1081                                    b"from" => from = Some(value.into_owned()),
1082                                    b"to" => to = Some(value.into_owned()),
1083                                    _ => {}
1084                                }
1085                            }
1086
1087                            if let (Some(from), Some(to)) = (from, to) {
1088                                arcs.push(FootnoteArc { from, to });
1089                            }
1090                        }
1091                        b"footnote" => {
1092                            let mut label = None;
1093                            let mut lang = None;
1094
1095                            for attribute in event.attributes() {
1096                                let attribute = attribute.map_err(|err| XbrlError::XmlParse {
1097                                    path: self.path.clone(),
1098                                    position: self.reader.buffer_position(),
1099                                    element: Some("footnote".to_string()),
1100                                    source: err.into(),
1101                                })?;
1102                                let local_name = attribute.key.local_name();
1103                                let value =
1104                                    attribute.decode_and_unescape_value(self.reader.decoder())?;
1105
1106                                match local_name.as_ref() {
1107                                    b"label" => label = Some(value.into_owned()),
1108                                    b"lang" => lang = Some(value.into_owned()),
1109                                    _ => {}
1110                                }
1111                            }
1112
1113                            // Read footnote text content
1114                            let mut text = String::new();
1115                            let mut text_buf = Vec::new();
1116                            loop {
1117                                match self.reader.read_event_into(&mut text_buf)? {
1118                                    Event::Text(ref t) => {
1119                                        let decoded =
1120                                            t.xml_content().map_err(quick_xml::Error::from)?;
1121                                        text.push_str(&decoded);
1122                                    }
1123                                    Event::End(ref e) if e.local_name().as_ref() == b"footnote" => {
1124                                        break;
1125                                    }
1126                                    Event::Eof => break,
1127                                    _ => {}
1128                                }
1129                                text_buf.clear();
1130                            }
1131
1132                            if let Some(label) = label {
1133                                footnotes.push(FootnoteResource {
1134                                    label,
1135                                    lang,
1136                                    text: text.trim().to_string(),
1137                                });
1138                            }
1139                        }
1140                        _ => {}
1141                    }
1142                }
1143                Event::End(ref event) if event.local_name().as_ref() == b"footnoteLink" => {
1144                    break;
1145                }
1146                Event::Eof => break,
1147                _ => {}
1148            }
1149            buf.clear();
1150        }
1151
1152        instance.footnote_links.push(RawFootnoteLink {
1153            role,
1154            locators,
1155            arcs,
1156            footnotes,
1157        });
1158
1159        Ok(())
1160    }
1161}
1162
1163#[cfg(test)]
1164mod tests {
1165    use super::*;
1166    use assert_matches::assert_matches;
1167    use std::str::FromStr;
1168
1169    #[test]
1170    fn test_parse_instance_root() {
1171        let xml = r#"<xbrli:xbrl xmlns:xbrli="http://www.xbrl.org/2003/instance"
1172                            xmlns:ifrs="http://xbrl.ifrs.org/taxonomy/2023">
1173                        </xbrli:xbrl>"#;
1174        let mut parser = InstanceParser::from_reader(xml.as_bytes());
1175        let instance = parser.parse_instance().unwrap();
1176
1177        assert_eq!(instance.namespaces.len(), 2);
1178        assert_eq!(
1179            instance
1180                .namespaces
1181                .get(&NamespacePrefix::from("xbrli"))
1182                .unwrap(),
1183            &NamespaceUri::from("http://www.xbrl.org/2003/instance")
1184        );
1185        assert_eq!(
1186            instance
1187                .namespaces
1188                .get(&NamespacePrefix::from("ifrs"))
1189                .unwrap(),
1190            &NamespaceUri::from("http://xbrl.ifrs.org/taxonomy/2023")
1191        );
1192    }
1193
1194    #[test]
1195    fn test_parse_schema_ref() {
1196        let xml = r#"<xbrli:xbrl xmlns:xbrli="http://www.xbrl.org/2003/instance"
1197                            xmlns:ifrs="http://xbrl.ifrs.org/taxonomy/2023">
1198                            <link:schemaRef xlink:href="ifrs.xsd" />
1199                        </xbrli:xbrl>"#;
1200        let mut parser = InstanceParser::from_reader(xml.as_bytes());
1201        let instance = parser.parse_instance().unwrap();
1202
1203        assert_eq!(instance.schema_refs.len(), 1);
1204        assert_eq!(instance.schema_refs[0].href, "ifrs.xsd");
1205    }
1206
1207    #[test]
1208    fn test_parse_role_ref() {
1209        let xml = r#"<xbrli:xbrl xmlns:xbrli="http://www.xbrl.org/2003/instance"
1210                            xmlns:ifrs="http://xbrl.ifrs.org/taxonomy/2023">
1211                            <link:roleRef roleURI="http://example.com/role" xlink:href="role.xml" />
1212                        </xbrli:xbrl>"#;
1213        let mut parser = InstanceParser::from_reader(xml.as_bytes());
1214        let instance = parser.parse_instance().unwrap();
1215
1216        assert_eq!(instance.role_refs.len(), 1);
1217        assert_eq!(instance.role_refs[0].role_uri, "http://example.com/role");
1218        assert_eq!(instance.role_refs[0].href, "role.xml");
1219    }
1220
1221    #[test]
1222    fn test_parse_arcrole_ref() {
1223        let xml = r#"<xbrli:xbrl xmlns:xbrli="http://www.xbrl.org/2003/instance"
1224                            xmlns:ifrs="http://xbrl.ifrs.org/taxonomy/2023">
1225                            <link:arcroleRef arcroleURI="http://example.com/arcrole" xlink:href="arcrole.xml" />
1226                        </xbrli:xbrl>"#;
1227        let mut parser = InstanceParser::from_reader(xml.as_bytes());
1228        let instance = parser.parse_instance().unwrap();
1229
1230        assert_eq!(instance.arcrole_refs.len(), 1);
1231        assert_eq!(
1232            instance.arcrole_refs[0].arcrole_uri,
1233            "http://example.com/arcrole"
1234        );
1235        assert_eq!(instance.arcrole_refs[0].href, "arcrole.xml");
1236    }
1237
1238    #[test]
1239    fn test_parse_context() {
1240        let xml = r#"<xbrli:xbrl
1241                                xmlns:xbrli="http://www.xbrl.org/2003/instance"
1242                                xmlns:xbrldi="http://xbrl.org/2006/xbrldi"
1243                                xmlns:ifrs="http://xbrl.ifrs.org/taxonomy/2023">
1244                                <context id="c1">
1245                                    <entity>
1246                                        <identifier scheme="http://example.com">ABC</identifier>
1247                                        <segment>
1248                                            <xbrldi:explicitMember dimension="ifrs:OperatingSegmentsAxis">
1249                                                ifrs:EuropeSegmentMember
1250                                            </xbrldi:explicitMember>
1251                                        </segment>
1252                                    </entity>
1253                                    <period>
1254                                        <instant>2024-12-31</instant>
1255                                    </period>
1256                                    <scenario>
1257                                        <xbrldi:explicitMember dimension="ifrs:ProductsAndServicesAxis">
1258                                            ifrs:SoftwareMember
1259                                        </xbrldi:explicitMember>
1260                                    </scenario>
1261                                </context>
1262                            </xbrli:xbrl>"#;
1263        let mut parser = InstanceParser::from_reader(xml.as_bytes());
1264        let instance = parser.parse_instance().unwrap();
1265
1266        assert_eq!(instance.contexts.len(), 1);
1267        let context = &instance.contexts[0];
1268        assert_eq!(
1269            context,
1270            &RawContext {
1271                id: "c1".to_string(),
1272                entity: RawEntity {
1273                    identifier: "ABC".to_string(),
1274                    scheme: "http://example.com".to_string(),
1275                    segment_dimensions: vec![RawDimension {
1276                        dimension: QName::from_str("ifrs:OperatingSegmentsAxis").unwrap(),
1277                        member: QName::from_str("ifrs:EuropeSegmentMember").unwrap(),
1278                    }],
1279                },
1280                period: RawPeriod::Instant("2024-12-31".to_string()),
1281                scenario_dimensions: vec![RawDimension {
1282                    dimension: QName::from_str("ifrs:ProductsAndServicesAxis").unwrap(),
1283                    member: QName::from_str("ifrs:SoftwareMember").unwrap(),
1284                }],
1285            }
1286        );
1287    }
1288
1289    #[test]
1290    fn test_parse_unit() {
1291        let xml = r#"<xbrli:xbrl xmlns:xbrli="http://www.xbrl.org/2003/instance"
1292                            xmlns:ifrs="http://xbrl.ifrs.org/taxonomy/2023">
1293                            <unit id="u1">
1294                                <measure>iso4217:EUR</measure>
1295                            </unit>
1296                        </xbrli:xbrl>"#;
1297        let mut parser = InstanceParser::from_reader(xml.as_bytes());
1298        let instance = parser.parse_instance().unwrap();
1299
1300        assert_eq!(instance.units.len(), 1);
1301        let unit = &instance.units[0];
1302        assert_eq!(
1303            unit,
1304            &RawUnit {
1305                id: "u1".to_string(),
1306                numerator: vec![QName::from_str("iso4217:EUR").unwrap()],
1307                denominator: vec![],
1308            }
1309        );
1310    }
1311
1312    #[test]
1313    fn test_parse_unit_divide() {
1314        let xml = r#"<xbrli:xbrl xmlns:xbrli="http://www.xbrl.org/2003/instance"
1315                                xmlns:ifrs="http://xbrl.ifrs.org/taxonomy/2023">
1316                                <xbrli:unit id="USD_per_share">
1317                                    <xbrli:divide>
1318                                        <xbrli:unitNumerator>
1319                                            <xbrli:measure>iso4217:USD</xbrli:measure>
1320                                        </xbrli:unitNumerator>
1321                                        <xbrli:unitDenominator>
1322                                            <xbrli:measure>xbrli:shares</xbrli:measure>
1323                                        </xbrli:unitDenominator>
1324                                    </xbrli:divide>
1325                                </xbrli:unit>
1326                            </xbrli:xbrl>"#;
1327        let mut parser = InstanceParser::from_reader(xml.as_bytes());
1328        let instance = parser.parse_instance().unwrap();
1329
1330        assert_eq!(instance.units.len(), 1);
1331        let unit = &instance.units[0];
1332        assert_eq!(
1333            unit,
1334            &RawUnit {
1335                id: "USD_per_share".to_string(),
1336                numerator: vec![QName::from_str("iso4217:USD").unwrap()],
1337                denominator: vec![QName::from_str("xbrli:shares").unwrap()],
1338            }
1339        );
1340    }
1341
1342    #[test]
1343    fn test_parse_item_fact() {
1344        let xml = r#"<xbrli:xbrl xmlns:xbrli="http://www.xbrl.org/2003/instance"
1345                            xmlns:ifrs="http://xbrl.ifrs.org/taxonomy/2023">
1346                            <ifrs:Revenue contextRef="c1" unitRef="u1" decimals="-3">
1347                                1200000
1348                            </ifrs:Revenue>
1349                        </xbrli:xbrl>"#;
1350        let mut parser = InstanceParser::from_reader(xml.as_bytes());
1351        let instance = parser.parse_instance().unwrap();
1352
1353        assert_eq!(instance.facts.len(), 1);
1354        let fact = &instance.facts[0];
1355        assert_matches!(fact, RawFact::Item(fact) => {
1356            assert_eq!(fact.name.to_string(), "ifrs:Revenue");
1357            assert_eq!(fact.value, "1200000");
1358            assert_eq!(fact.context_ref, "c1");
1359            assert_eq!(fact.unit_ref.as_deref(), Some("u1"));
1360            assert_eq!(fact.decimals.as_deref(), Some("-3"));
1361            assert!(!fact.is_nil);
1362        });
1363    }
1364
1365    #[test]
1366    fn test_parse_tuple_fact() {
1367        let xml = r#"<xbrli:xbrl xmlns:xbrli="http://www.xbrl.org/2003/instance"
1368                                xmlns:t="http://example.com/taxonomy">
1369                                <t:Address>
1370                                    <t:Street contextRef="c1">Main Street</t:Street>
1371                                    <t:City contextRef="c1">Berlin</t:City>
1372                                </t:Address>
1373                            </xbrli:xbrl>"#;
1374        let mut parser = InstanceParser::from_reader(xml.as_bytes());
1375        let instance = parser.parse_instance().unwrap();
1376
1377        assert_eq!(instance.facts.len(), 1);
1378        let fact = &instance.facts[0];
1379        assert_matches!(fact, RawFact::Tuple(tuple) => {
1380            assert_eq!(tuple.name.to_string(), "t:Address");
1381            assert!(!tuple.is_nil);
1382            assert_eq!(tuple.children.len(), 2);
1383
1384            assert_matches!(&tuple.children[0], RawFact::Item(item) => {
1385                assert_eq!(item.name.to_string(), "t:Street");
1386                assert_eq!(item.value, "Main Street");
1387                assert_eq!(item.context_ref, "c1");
1388            });
1389            assert_matches!(&tuple.children[1], RawFact::Item(item) => {
1390                assert_eq!(item.name.to_string(), "t:City");
1391                assert_eq!(item.value, "Berlin");
1392                assert_eq!(item.context_ref, "c1");
1393            });
1394        });
1395    }
1396
1397    #[test]
1398    fn test_parse_nested_tuple() {
1399        let xml = r#"<xbrli:xbrl xmlns:xbrli="http://www.xbrl.org/2003/instance"
1400                                xmlns:t="http://example.com/taxonomy">
1401                                <t:Outer>
1402                                    <t:Inner>
1403                                        <t:Value contextRef="c1">42</t:Value>
1404                                    </t:Inner>
1405                                </t:Outer>
1406                            </xbrli:xbrl>"#;
1407        let mut parser = InstanceParser::from_reader(xml.as_bytes());
1408        let instance = parser.parse_instance().unwrap();
1409
1410        assert_eq!(instance.facts.len(), 1);
1411        let fact = &instance.facts[0];
1412        assert_matches!(fact, RawFact::Tuple(outer) => {
1413            assert_eq!(outer.name.to_string(), "t:Outer");
1414            assert!(!outer.is_nil);
1415            assert_eq!(outer.children.len(), 1);
1416
1417            assert_matches!(&outer.children[0], RawFact::Tuple(inner) => {
1418                assert_eq!(inner.name.to_string(), "t:Inner");
1419                assert!(!inner.is_nil);
1420                assert_eq!(inner.children.len(), 1);
1421
1422                assert_matches!(&inner.children[0], RawFact::Item(item) => {
1423                    assert_eq!(item.name.to_string(), "t:Value");
1424                    assert_eq!(item.value, "42");
1425                    assert_eq!(item.context_ref, "c1");
1426                });
1427            });
1428        });
1429    }
1430
1431    #[test]
1432    fn test_parse_nil_item_fact() {
1433        let xml = r#"<xbrli:xbrl xmlns:xbrli="http://www.xbrl.org/2003/instance"
1434                            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1435                            xmlns:ifrs="http://xbrl.ifrs.org/taxonomy/2023">
1436                            <ifrs:Revenue contextRef="c1" xsi:nil="true" />
1437                        </xbrli:xbrl>"#;
1438        let mut parser = InstanceParser::from_reader(xml.as_bytes());
1439        let instance = parser.parse_instance().unwrap();
1440
1441        assert_eq!(instance.facts.len(), 1);
1442        match &instance.facts[0] {
1443            RawFact::Item(fact) => {
1444                assert_eq!(fact.name.to_string(), "ifrs:Revenue");
1445                assert!(fact.is_nil);
1446                assert_eq!(fact.value, "");
1447                assert_eq!(fact.context_ref, "c1");
1448            }
1449            RawFact::Tuple(_) => panic!("expected item fact"),
1450        }
1451    }
1452
1453    #[test]
1454    fn test_parse_empty_tuple() {
1455        let xml = r#"<xbrli:xbrl xmlns:xbrli="http://www.xbrl.org/2003/instance"
1456                            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1457                            xmlns:t="http://example.com/taxonomy">
1458                            <t:Address xsi:nil="true" />
1459                        </xbrli:xbrl>"#;
1460        let mut parser = InstanceParser::from_reader(xml.as_bytes());
1461        let instance = parser.parse_instance().unwrap();
1462
1463        assert_eq!(instance.facts.len(), 1);
1464        match &instance.facts[0] {
1465            RawFact::Tuple(tuple) => {
1466                assert_eq!(tuple.name.to_string(), "t:Address");
1467                assert!(tuple.is_nil);
1468                assert!(tuple.children.is_empty());
1469            }
1470            RawFact::Item(_) => panic!("expected tuple fact"),
1471        }
1472    }
1473
1474    #[test]
1475    fn test_parse_footnote_link() {
1476        let xml = r##"<xbrli:xbrl xmlns:xbrli="http://www.xbrl.org/2003/instance"
1477                            xmlns:ifrs="http://xbrl.ifrs.org/taxonomy/2023">
1478                            <link:footnoteLink role="http://example.com/footnote">
1479                                <link:loc xlink:label="loc1" xlink:href="#c1" />
1480                                <link:footnote xlink:label="fn1" xml:lang="en">
1481                                    This is a footnote.
1482                                </link:footnote>
1483                                <link:footnoteArc xlink:from="loc1" xlink:to="fn1" />
1484                            </link:footnoteLink>
1485                        </xbrli:xbrl>"##;
1486        let mut parser = InstanceParser::from_reader(xml.as_bytes());
1487        let instance = parser.parse_instance().unwrap();
1488
1489        assert_eq!(instance.footnote_links.len(), 1);
1490        let footnote_link = &instance.footnote_links[0];
1491        assert_eq!(
1492            footnote_link,
1493            &RawFootnoteLink {
1494                role: "http://example.com/footnote".to_string(),
1495                locators: vec![Locator {
1496                    label: "loc1".to_string(),
1497                    href: "#c1".to_string(),
1498                }],
1499                arcs: vec![FootnoteArc {
1500                    from: "loc1".to_string(),
1501                    to: "fn1".to_string(),
1502                }],
1503                footnotes: vec![FootnoteResource {
1504                    label: "fn1".to_string(),
1505                    lang: Some("en".to_string()),
1506                    text: "This is a footnote.".to_string(),
1507                }],
1508            }
1509        );
1510    }
1511
1512    #[test]
1513    fn test_parse_instance() {
1514        let xml = r#"<xbrli:xbrl xmlns:xbrli="http://www.xbrl.org/2003/instance"
1515                            xmlns:ifrs="http://xbrl.ifrs.org/taxonomy/2023">
1516                            <link:schemaRef xlink:href="ifrs.xsd" />
1517                            <context id="c1">
1518                                <entity>
1519                                    <identifier scheme="http://example.com">ABC</identifier>
1520                                </entity>
1521                                <period>
1522                                    <instant>2024-12-31</instant>
1523                                </period>
1524                            </context>
1525                            <unit id="u1">
1526                                <measure>iso4217:EUR</measure>
1527                            </unit>
1528                            <ifrs:Revenue contextRef="c1" unitRef="u1" decimals="-3">
1529                                1200000
1530                            </ifrs:Revenue>
1531                        </xbrli:xbrl>"#;
1532        let mut parser = InstanceParser::from_reader(xml.as_bytes());
1533        let instance = parser.parse_instance().unwrap();
1534
1535        assert_eq!(instance.contexts.len(), 1);
1536        assert_eq!(instance.units.len(), 1);
1537        assert_eq!(instance.facts.len(), 1);
1538
1539        assert_eq!(
1540            instance,
1541            RawInstance {
1542                namespaces: {
1543                    let mut namespaces = HashMap::new();
1544                    namespaces.insert("xbrli".into(), "http://www.xbrl.org/2003/instance".into());
1545                    namespaces.insert("ifrs".into(), "http://xbrl.ifrs.org/taxonomy/2023".into());
1546                    namespaces
1547                },
1548                schema_refs: vec![SchemaRef {
1549                    href: "ifrs.xsd".to_string(),
1550                }],
1551                role_refs: vec![],
1552                arcrole_refs: vec![],
1553                contexts: vec![RawContext {
1554                    id: "c1".to_string(),
1555                    entity: RawEntity {
1556                        identifier: "ABC".to_string(),
1557                        scheme: "http://example.com".to_string(),
1558                        segment_dimensions: vec![],
1559                    },
1560                    period: RawPeriod::Instant("2024-12-31".to_string()),
1561                    scenario_dimensions: vec![],
1562                }],
1563                units: vec![RawUnit {
1564                    id: "u1".to_string(),
1565                    numerator: vec![QName::from_str("iso4217:EUR").unwrap()],
1566                    denominator: vec![],
1567                }],
1568                facts: vec![RawFact::Item(RawItemFact {
1569                    name: QName::from_str("ifrs:Revenue").unwrap(),
1570                    value: "1200000".to_string(),
1571                    context_ref: "c1".to_string(),
1572                    unit_ref: Some("u1".to_string()),
1573                    decimals: Some("-3".to_string()),
1574                    precision: None,
1575                    id: None,
1576                    is_nil: false,
1577                })],
1578                footnote_links: vec![],
1579            }
1580        );
1581    }
1582}