Skip to main content

xbrl_rs/taxonomy/linkbases/
parser.rs

1use crate::{RoleUri, XbrlError, xml::ArcroleUri};
2use quick_xml::{
3    Reader,
4    events::{BytesStart, Event},
5};
6use rust_decimal::Decimal;
7use std::{
8    fs::File,
9    io::{BufRead, BufReader},
10    path::{Path, PathBuf},
11    str,
12};
13
14/// A locator in a presentation, calculation, or definition link.
15#[derive(Debug, PartialEq, Eq)]
16pub struct Locator {
17    /// The label of the locator, used to reference it in arcs.
18    pub label: String,
19    /// The href of the locator, pointing to the element in the taxonomy.
20    pub href: String,
21}
22
23/// A resource in a label link.
24#[derive(Debug, PartialEq, Eq)]
25pub struct LabelResource {
26    /// The label of the resource, used to reference it in arcs.
27    pub label: String,
28    /// The role of the resource, used to specify the type of label.
29    pub role: Option<String>,
30    /// The language of the resource (e.g., "en", "de").
31    pub lang: String,
32    /// The text content of the resource, containing the label information.
33    pub text: String,
34}
35
36/// A resource in a reference link.
37///
38/// Note: The XBRL specification does not define a specific structure for
39/// reference resources, but they are typically used to provide additional
40/// information about the referenced elements, such as citations or
41/// explanations. Therefore, we can use a similar structure to `LabelResource`,
42/// but without parsing the text content, since it can be arbitrary and may
43/// contain complex XML content.
44#[derive(Debug, PartialEq, Eq)]
45pub struct ReferenceResource {
46    /// The label of the resource, used to reference it in arcs.
47    pub label: String,
48    /// The role of the resource, used to specify the type of reference.
49    pub role: Option<String>,
50}
51
52/// A parent-child relationship from a presentation linkbase.
53#[derive(Debug, Clone, PartialEq)]
54pub struct RawPresentationArc {
55    /// Parent concept locator label.
56    pub from: String,
57    /// Child concept locator label.
58    pub to: String,
59    /// Display order among siblings.
60    pub order: Option<Decimal>,
61    /// The preferred label role URI, if specified.
62    pub preferred_label: Option<RoleUri>,
63    /// The arc role URI (e.g., `http://www.xbrl.org/2003/arcrole/parent-child`).
64    pub arcrole: ArcroleUri,
65}
66
67/// A summation-item relationship from a calculation linkbase.
68#[derive(Debug, Clone, PartialEq)]
69pub struct RawCalculationArc {
70    /// Parent (summation) concept element ID.
71    pub from: String,
72    /// Child (contributing item) concept element ID.
73    pub to: String,
74    /// Display order among siblings.
75    pub order: Option<Decimal>,
76    /// Weight factor (typically 1.0 or -1.0).
77    pub weight: Decimal,
78    /// The arc role URI (e.g., `http://www.xbrl.org/2003/arcrole/summation-item`).
79    pub arcrole: ArcroleUri,
80}
81
82/// A dimensional relationship from a definition linkbase.
83#[derive(Debug, Clone, PartialEq)]
84pub struct RawDefinitionArc {
85    /// Source concept element ID.
86    pub from: String,
87    /// Target concept element ID.
88    pub to: String,
89    /// Display/processing order.
90    pub order: Option<Decimal>,
91    /// The arc role URI.
92    pub arcrole: ArcroleUri,
93}
94
95/// An arc in a label link, connecting a locator to a resource.
96#[derive(Debug, PartialEq, Eq)]
97pub struct RawLabelArc {
98    /// The label of the source locator, referencing the `xlink:label` of a
99    /// `loc` element.
100    pub from: String,
101    /// The label of the target resource, referencing the `xlink:label` of a
102    /// `resource` element.
103    pub to: String,
104}
105
106/// An arc in a reference link, connecting a locator to a resource.
107#[derive(Debug, PartialEq, Eq)]
108pub struct RawReferenceArc {
109    /// The label of the source locator, referencing the `xlink:label` of a
110    /// `loc` element.
111    pub from: String,
112    /// The label of the target resource, referencing the `xlink:label` of a
113    /// `resource` element.
114    pub to: String,
115}
116
117/// A presentation link, containing locators and arcs.
118#[derive(Debug, PartialEq)]
119pub struct PresentationLink {
120    /// The role of the presentation link, used to specify the type of
121    /// presentation.
122    pub role: String,
123    /// The locators in the presentation link, used to reference elements in the
124    /// taxonomy.
125    pub locators: Vec<Locator>,
126    /// The arcs in the presentation link, used to specify the relationships
127    /// between locators.
128    pub arcs: Vec<RawPresentationArc>,
129}
130
131/// A calculation link, containing locators and arcs.
132#[derive(Debug, PartialEq)]
133pub struct CalculationLink {
134    /// The role of the calculation link, used to specify the type of
135    /// calculation.
136    pub role: String,
137    /// The locators in the calculation link, used to reference elements in the
138    /// taxonomy.
139    pub locators: Vec<Locator>,
140    /// The arcs in the calculation link, used to specify the relationships
141    /// between locators.
142    pub arcs: Vec<RawCalculationArc>,
143}
144
145/// A definition link, containing locators and arcs.
146#[derive(Debug, PartialEq)]
147pub struct DefinitionLink {
148    /// The role of the definition link, used to specify the type of
149    /// relationship.
150    pub role: String,
151    /// The locators in the definition link, used to reference elements in the
152    /// taxonomy.
153    pub locators: Vec<Locator>,
154    /// The arcs in the definition link, used to specify the relationships
155    /// between locators.
156    pub arcs: Vec<RawDefinitionArc>,
157}
158
159/// A reference link, containing locators, arcs, and resources.
160#[derive(Debug, PartialEq, Eq)]
161pub struct ReferenceLink {
162    /// The role of the reference link, used to specify the type of
163    /// relationship.
164    pub role: String,
165    /// The locators in the reference link, used to reference elements in the
166    /// taxonomy.
167    pub locators: Vec<Locator>,
168    /// The arcs in the reference link, used to specify the relationships
169    /// between locators.
170    pub arcs: Vec<RawReferenceArc>,
171    /// The resources in the reference link, used to provide additional
172    /// information about the referenced elements.
173    pub references: Vec<ReferenceResource>,
174}
175
176/// A label link, containing locators, arcs, and resources.
177///
178/// Locators reference concepts in the taxonomy, arcs connect locators to label
179/// resources, and label resources contain the actual label text and metadata:
180/// ```text
181/// Concept (in schema)
182///       ↑
183///    locator
184///       │
185///       │ arc
186///       ▼
187///    resource
188///       ↓
189///  human-readable content
190/// ```
191#[derive(Debug, PartialEq, Eq)]
192pub struct LabelLink {
193    /// The role of the label link, used to specify the type of
194    /// relationship.
195    pub role: String,
196    /// The locators in the label link, used to reference elements in the
197    /// taxonomy.
198    pub locators: Vec<Locator>,
199    /// The arcs in the label link, used to specify the relationships
200    /// between locators.
201    pub arcs: Vec<RawLabelArc>,
202    /// The labels in the label link, used to provide additional
203    /// information about the referenced elements.
204    pub labels: Vec<LabelResource>,
205}
206
207/// The complete linkbase, containing all types of links.
208#[derive(Debug, PartialEq, Default)]
209pub struct RawLinkbases {
210    /// The presentation links in the linkbase, used to specify the presentation
211    /// of elements in the taxonomy.
212    pub presentation_links: Vec<PresentationLink>,
213    /// The calculation links in the linkbase, used to specify the calculations
214    /// between elements in the taxonomy.
215    pub calculation_links: Vec<CalculationLink>,
216    /// The definition links in the linkbase, used to specify the relationships
217    /// between elements in the taxonomy.
218    pub definition_links: Vec<DefinitionLink>,
219    /// The label links in the linkbase, used to specify the labels of elements
220    /// in the taxonomy.
221    pub label_links: Vec<LabelLink>,
222    /// The reference links in the linkbase, used to specify the references of
223    /// elements in the taxonomy.
224    pub reference_links: Vec<ReferenceLink>,
225}
226
227impl RawLinkbases {
228    /// Creates a new `Linkbase` with the given links.
229    pub fn new(
230        presentation_links: Vec<PresentationLink>,
231        calculation_links: Vec<CalculationLink>,
232        definition_links: Vec<DefinitionLink>,
233        label_links: Vec<LabelLink>,
234        reference_links: Vec<ReferenceLink>,
235    ) -> Self {
236        Self {
237            presentation_links,
238            calculation_links,
239            definition_links,
240            label_links,
241            reference_links,
242        }
243    }
244}
245
246/// Parses a string into a Decimal.
247fn parse_decimal(value: &str) -> Result<Decimal, XbrlError> {
248    value.parse::<Decimal>().map_err(|_| XbrlError::ParseError {
249        expected: "floating point number",
250        value: value.to_string(),
251    })
252}
253
254/// The parser for XBRL linkbase documents.
255pub struct LinkbaseParser<R> {
256    /// Path of the currently parsed linkbase file if available. Used for error
257    /// reporting.
258    path: Option<PathBuf>,
259    /// The XML reader for the linkbase document.
260    reader: Reader<R>,
261}
262
263impl LinkbaseParser<BufReader<File>> {
264    /// Creates a new `LinkbaseParser` from the file at the given path.
265    pub fn from_file(path: &Path) -> Result<Self, XbrlError> {
266        let file = File::open(path).map_err(|err| XbrlError::FileOpen {
267            path: path.to_path_buf(),
268            context: "opening file".to_string(),
269            source: err,
270        })?;
271        let mut reader = Reader::from_reader(BufReader::new(file));
272
273        reader.config_mut().trim_text_start = true;
274        reader.config_mut().trim_text_end = true;
275
276        Ok(Self {
277            path: Some(path.to_path_buf()),
278            reader,
279        })
280    }
281}
282
283impl<R: BufRead> LinkbaseParser<R> {
284    /// Creates a new `LinkbaseParser` from the given XML reader.
285    pub fn new(reader: Reader<R>) -> Self {
286        Self { path: None, reader }
287    }
288
289    /// Creates a new `LinkbaseParser` from the given BufReader.
290    pub fn from_reader(reader: R) -> Self {
291        let mut reader = Reader::from_reader(reader);
292
293        reader.config_mut().trim_text_start = true;
294        reader.config_mut().trim_text_end = true;
295
296        Self { path: None, reader }
297    }
298
299    /// Parses the linkbase document and fills the provided `Linkbases` struct
300    /// with the parsed links.
301    pub fn parse_linkbase(&mut self, linkbase: &mut RawLinkbases) -> Result<(), XbrlError> {
302        let mut buf = Vec::new();
303        let mut has_linkbase_root = false;
304
305        loop {
306            match self.reader.read_event_into(&mut buf)? {
307                quick_xml::events::Event::Start(event) => match event.local_name().as_ref() {
308                    b"linkbase" => {
309                        has_linkbase_root = true;
310                    }
311                    b"presentationLink" => {
312                        let link = self.parse_presentation_link(event)?;
313                        linkbase.presentation_links.push(link);
314                    }
315
316                    b"calculationLink" => {
317                        let link = self.parse_calculation_link(event)?;
318                        linkbase.calculation_links.push(link);
319                    }
320
321                    b"definitionLink" => {
322                        let link = self.parse_definition_link(event)?;
323                        linkbase.definition_links.push(link);
324                    }
325
326                    b"labelLink" => {
327                        let link = self.parse_label_link(event)?;
328                        linkbase.label_links.push(link);
329                    }
330
331                    b"referenceLink" => {
332                        let link = self.parse_reference_link(event)?;
333                        linkbase.reference_links.push(link);
334                    }
335
336                    _ => {}
337                },
338                Event::Eof => break,
339
340                _ => {}
341            }
342
343            buf.clear();
344        }
345
346        if !has_linkbase_root {
347            return Err(XbrlError::InvalidLinkbaseDocument {
348                path: self.path.clone(),
349                reason: "missing <linkbase> root element".to_string(),
350            });
351        }
352
353        Ok(())
354    }
355
356    /// Parses a `presentationLink` element and its children.
357    fn parse_presentation_link(
358        &mut self,
359        start: BytesStart,
360    ) -> Result<PresentationLink, XbrlError> {
361        // Extract the `xlink:role` attribute
362        let mut role = None;
363        for attribute in start.attributes() {
364            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
365                path: self.path.clone(),
366                position: self.reader.buffer_position(),
367                element: Some("presentationLink".to_string()),
368                source: err.into(),
369            })?;
370
371            if attribute.key.as_ref() == b"xlink:role" {
372                let value = attribute.decode_and_unescape_value(self.reader.decoder())?;
373                role = Some(value.to_string());
374            }
375        }
376        let role = role.ok_or_else(|| XbrlError::ParseError {
377            expected: "xlink:role on presentationLink",
378            value: "".to_string(),
379        })?;
380
381        let mut locators = Vec::new();
382        let mut arcs = Vec::new();
383        let mut buf = Vec::new();
384
385        loop {
386            match self.reader.read_event_into(&mut buf)? {
387                Event::Start(event) | Event::Empty(event) => match event.local_name().as_ref() {
388                    b"loc" => {
389                        let locator = self.parse_locator(&event)?;
390                        locators.push(locator);
391                    }
392                    b"presentationArc" => {
393                        // Parse arc
394                        let mut from = None;
395                        let mut to = None;
396                        let mut order = None;
397                        let mut preferred_label = None;
398                        let mut arcrole = None;
399
400                        for attribute in event.attributes() {
401                            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
402                                path: self.path.clone(),
403                                position: self.reader.buffer_position(),
404                                element: Some("presentationLink".to_string()),
405                                source: err.into(),
406                            })?;
407
408                            match attribute.key.as_ref() {
409                                b"xlink:from" => {
410                                    let value = attribute
411                                        .decode_and_unescape_value(self.reader.decoder())?;
412                                    from = Some(value.to_string())
413                                }
414                                b"xlink:to" => {
415                                    let value = attribute
416                                        .decode_and_unescape_value(self.reader.decoder())?;
417                                    to = Some(value.to_string())
418                                }
419                                b"order" => {
420                                    let value = attribute
421                                        .decode_and_unescape_value(self.reader.decoder())?;
422                                    order = Some(parse_decimal(&value)?);
423                                }
424                                b"preferredLabel" => {
425                                    let value = attribute
426                                        .decode_and_unescape_value(self.reader.decoder())?;
427                                    preferred_label = Some(value.to_string())
428                                }
429                                b"xlink:arcrole" => {
430                                    let value = attribute
431                                        .decode_and_unescape_value(self.reader.decoder())?;
432                                    arcrole = Some(value.to_string())
433                                }
434                                _ => {}
435                            }
436                        }
437
438                        let arc = RawPresentationArc {
439                            from: from.ok_or_else(|| XbrlError::ParseError {
440                                expected: "xlink:from on presentationArc",
441                                value: "".to_string(),
442                            })?,
443                            to: to.ok_or_else(|| XbrlError::ParseError {
444                                expected: "xlink:to on presentationArc",
445                                value: "".to_string(),
446                            })?,
447                            arcrole: arcrole
448                                .ok_or_else(|| XbrlError::ParseError {
449                                    expected: "xlink:arcrole on presentationArc",
450                                    value: "".to_string(),
451                                })?
452                                .into(),
453                            order,
454                            preferred_label: preferred_label.map(RoleUri::from),
455                        };
456                        arcs.push(arc);
457                    }
458
459                    _ => {}
460                },
461                Event::End(event) => {
462                    if event.name() == start.name() {
463                        break;
464                    }
465                }
466                Event::Eof => {
467                    return Err(XbrlError::ParseError {
468                        expected: "presentationLink end tag",
469                        value: "".to_string(),
470                    });
471                }
472
473                _ => {}
474            }
475
476            buf.clear();
477        }
478
479        Ok(PresentationLink {
480            role,
481            locators,
482            arcs,
483        })
484    }
485
486    /// Parses a `calculationLink` element and its children.
487    fn parse_calculation_link(&mut self, start: BytesStart) -> Result<CalculationLink, XbrlError> {
488        // Extract the `xlink:role` attribute
489        let mut role = None;
490        for attribute in start.attributes() {
491            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
492                path: self.path.clone(),
493                position: self.reader.buffer_position(),
494                element: Some("calculationLink".to_string()),
495                source: err.into(),
496            })?;
497
498            if attribute.key.as_ref() == b"xlink:role" {
499                let value = attribute.decode_and_unescape_value(self.reader.decoder())?;
500                role = Some(value.to_string());
501            }
502        }
503        let role = role.ok_or_else(|| XbrlError::ParseError {
504            expected: "xlink:role on calculationLink",
505            value: "".to_string(),
506        })?;
507
508        let mut locators = Vec::new();
509        let mut arcs = Vec::new();
510        let mut buf = Vec::new();
511
512        loop {
513            match self.reader.read_event_into(&mut buf)? {
514                Event::Start(event) | Event::Empty(event) => match event.local_name().as_ref() {
515                    b"loc" => {
516                        let locator = self.parse_locator(&event)?;
517                        locators.push(locator);
518                    }
519                    b"calculationArc" => {
520                        // Parse a calculation arc
521                        let mut from = None;
522                        let mut to = None;
523                        let mut order = None;
524                        let mut weight = None;
525                        let mut arcrole = None;
526
527                        for attribute in event.attributes() {
528                            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
529                                path: self.path.clone(),
530                                position: self.reader.buffer_position(),
531                                element: Some("calculationLink".to_string()),
532                                source: err.into(),
533                            })?;
534
535                            match attribute.key.as_ref() {
536                                b"xlink:from" => {
537                                    let value = attribute
538                                        .decode_and_unescape_value(self.reader.decoder())?;
539                                    from = Some(value.to_string())
540                                }
541                                b"xlink:to" => {
542                                    let value = attribute
543                                        .decode_and_unescape_value(self.reader.decoder())?;
544                                    to = Some(value.to_string())
545                                }
546                                b"xlink:arcrole" => {
547                                    let value = attribute
548                                        .decode_and_unescape_value(self.reader.decoder())?;
549                                    arcrole = Some(value.to_string())
550                                }
551                                b"order" => {
552                                    let value = attribute
553                                        .decode_and_unescape_value(self.reader.decoder())?;
554                                    order = Some(parse_decimal(&value)?);
555                                }
556                                b"weight" => {
557                                    let value = attribute
558                                        .decode_and_unescape_value(self.reader.decoder())?;
559                                    weight = Some(parse_decimal(&value)?);
560                                }
561                                _ => {}
562                            }
563                        }
564
565                        arcs.push(RawCalculationArc {
566                            from: from.ok_or_else(|| XbrlError::ParseError {
567                                expected: "xlink:from on calculationArc",
568                                value: "".to_string(),
569                            })?,
570                            to: to.ok_or_else(|| XbrlError::ParseError {
571                                expected: "xlink:to on calculationArc",
572                                value: "".to_string(),
573                            })?,
574                            arcrole: arcrole
575                                .ok_or_else(|| XbrlError::ParseError {
576                                    expected: "xlink:arcrole on calculationArc",
577                                    value: "".to_string(),
578                                })?
579                                .into(),
580                            order,
581                            weight: weight.ok_or_else(|| XbrlError::ParseError {
582                                expected: "weight on calculationArc",
583                                value: "".to_string(),
584                            })?,
585                        });
586                    }
587                    _ => {}
588                },
589                Event::End(event) => {
590                    if event.name() == start.name() {
591                        break;
592                    }
593                }
594                Event::Eof => {
595                    return Err(XbrlError::ParseError {
596                        expected: "calculationLink end tag",
597                        value: "".to_string(),
598                    });
599                }
600                _ => {}
601            }
602
603            buf.clear();
604        }
605
606        Ok(CalculationLink {
607            role,
608            locators,
609            arcs,
610        })
611    }
612
613    /// Parses a `definitionLink` element and its children.
614    fn parse_definition_link(&mut self, start: BytesStart) -> Result<DefinitionLink, XbrlError> {
615        // Extract the `xlink:role` attribute
616        let mut role = None;
617        for attribute in start.attributes() {
618            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
619                path: self.path.clone(),
620                position: self.reader.buffer_position(),
621                element: Some("definitionLink".to_string()),
622                source: err.into(),
623            })?;
624
625            if attribute.key.as_ref() == b"xlink:role" {
626                let value = attribute.decode_and_unescape_value(self.reader.decoder())?;
627                role = Some(value.to_string());
628            }
629        }
630        let role = role.ok_or_else(|| XbrlError::ParseError {
631            expected: "xlink:role on definitionLink",
632            value: "".to_string(),
633        })?;
634
635        let mut locators = Vec::new();
636        let mut arcs = Vec::new();
637        let mut buf = Vec::new();
638
639        loop {
640            match self.reader.read_event_into(&mut buf)? {
641                Event::Start(event) | Event::Empty(event) => match event.local_name().as_ref() {
642                    b"loc" => {
643                        let locator = self.parse_locator(&event)?;
644                        locators.push(locator);
645                    }
646                    b"definitionArc" => {
647                        let mut from = None;
648                        let mut to = None;
649                        let mut arcrole = None;
650                        let mut order = None;
651
652                        for attribute in event.attributes() {
653                            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
654                                path: self.path.clone(),
655                                position: self.reader.buffer_position(),
656                                element: Some("definitionArc".to_string()),
657                                source: err.into(),
658                            })?;
659
660                            match attribute.key.as_ref() {
661                                b"xlink:from" => {
662                                    let value = attribute
663                                        .decode_and_unescape_value(self.reader.decoder())?;
664                                    from = Some(value.to_string());
665                                }
666                                b"xlink:to" => {
667                                    let value = attribute
668                                        .decode_and_unescape_value(self.reader.decoder())?;
669                                    to = Some(value.to_string());
670                                }
671                                b"xlink:arcrole" => {
672                                    let value = attribute
673                                        .decode_and_unescape_value(self.reader.decoder())?;
674                                    arcrole = Some(value.to_string());
675                                }
676                                b"order" => {
677                                    let value = attribute
678                                        .decode_and_unescape_value(self.reader.decoder())?;
679                                    order = Some(parse_decimal(&value)?);
680                                }
681                                _ => {}
682                            }
683                        }
684
685                        arcs.push(RawDefinitionArc {
686                            from: from.ok_or_else(|| XbrlError::ParseError {
687                                expected: "xlink:from on definitionArc",
688                                value: "".to_string(),
689                            })?,
690                            to: to.ok_or_else(|| XbrlError::ParseError {
691                                expected: "xlink:to on definitionArc",
692                                value: "".to_string(),
693                            })?,
694                            arcrole: arcrole
695                                .ok_or_else(|| XbrlError::ParseError {
696                                    expected: "xlink:arcrole on definitionArc",
697                                    value: "".to_string(),
698                                })?
699                                .into(),
700                            order,
701                        });
702                    }
703                    _ => {}
704                },
705                Event::End(event) => {
706                    if event.name() == start.name() {
707                        break;
708                    }
709                }
710                Event::Eof => {
711                    return Err(XbrlError::ParseError {
712                        expected: "definitionLink end tag",
713                        value: "".to_string(),
714                    });
715                }
716                _ => {}
717            }
718
719            buf.clear();
720        }
721
722        Ok(DefinitionLink {
723            role,
724            locators,
725            arcs,
726        })
727    }
728
729    /// Parses a `labelLink` element and its children.
730    fn parse_label_link(&mut self, start: BytesStart) -> Result<LabelLink, XbrlError> {
731        // Extract the `xlink:role` attribute
732        let mut role = String::new();
733
734        for attribute in start.attributes() {
735            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
736                path: self.path.clone(),
737                position: self.reader.buffer_position(),
738                element: Some("labelLink".to_string()),
739                source: err.into(),
740            })?;
741
742            if attribute.key.as_ref() == b"xlink:role" {
743                let value = attribute.decode_and_unescape_value(self.reader.decoder())?;
744                role = value.to_string();
745            }
746        }
747
748        let mut locators = Vec::new();
749        let mut arcs = Vec::new();
750        let mut labels = Vec::new();
751        let mut buf = Vec::new();
752
753        loop {
754            match self.reader.read_event_into(&mut buf)? {
755                Event::Start(event) | Event::Empty(event) => match event.local_name().as_ref() {
756                    b"loc" => {
757                        let locator = self.parse_locator(&event)?;
758                        locators.push(locator);
759                    }
760                    b"labelArc" => {
761                        let mut from = None;
762                        let mut to = None;
763
764                        for attribute in event.attributes() {
765                            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
766                                path: self.path.clone(),
767                                position: self.reader.buffer_position(),
768                                element: Some("labelArc".to_string()),
769                                source: err.into(),
770                            })?;
771
772                            match attribute.key.as_ref() {
773                                b"xlink:from" => {
774                                    let value = attribute
775                                        .decode_and_unescape_value(self.reader.decoder())?;
776                                    from = Some(value.to_string());
777                                }
778                                b"xlink:to" => {
779                                    let value = attribute
780                                        .decode_and_unescape_value(self.reader.decoder())?;
781                                    to = Some(value.to_string());
782                                }
783                                _ => {}
784                            }
785                        }
786
787                        arcs.push(RawLabelArc {
788                            from: from.ok_or_else(|| XbrlError::ParseError {
789                                expected: "xlink:from on labelArc",
790                                value: "".to_string(),
791                            })?,
792                            to: to.ok_or_else(|| XbrlError::ParseError {
793                                expected: "xlink:to on labelArc",
794                                value: "".to_string(),
795                            })?,
796                        });
797                    }
798                    b"label" => {
799                        let mut label = None;
800                        let mut label_role = None;
801                        let mut label_lang = None;
802
803                        for attribute in event.attributes() {
804                            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
805                                path: self.path.clone(),
806                                position: self.reader.buffer_position(),
807                                element: Some("label".to_string()),
808                                source: err.into(),
809                            })?;
810
811                            match attribute.key.as_ref() {
812                                b"xlink:label" => {
813                                    let value = attribute
814                                        .decode_and_unescape_value(self.reader.decoder())?;
815                                    label = Some(value.to_string());
816                                }
817                                b"xlink:role" => {
818                                    let value = attribute
819                                        .decode_and_unescape_value(self.reader.decoder())?;
820                                    label_role = Some(value.to_string());
821                                }
822                                b"xml:lang" => {
823                                    let value = attribute
824                                        .decode_and_unescape_value(self.reader.decoder())?;
825                                    label_lang = Some(value.to_string());
826                                }
827                                _ => {}
828                            }
829                        }
830
831                        let mut text_buf = Vec::new();
832                        let bytes_text = self
833                            .reader
834                            .read_text_into(event.to_end().name(), &mut text_buf)?;
835                        let text = str::from_utf8(bytes_text.as_ref())
836                            .map_err(XbrlError::Utf8)?
837                            .trim()
838                            .to_string();
839
840                        labels.push(LabelResource {
841                            label: label.ok_or_else(|| XbrlError::ParseError {
842                                expected: "xlink:label on label",
843                                value: "".to_string(),
844                            })?,
845                            role: label_role,
846                            lang: label_lang.unwrap_or_default(),
847                            text,
848                        });
849                    }
850                    _ => {}
851                },
852                Event::End(event) => {
853                    if event.name() == start.name() {
854                        break;
855                    }
856                }
857                Event::Eof => {
858                    return Err(XbrlError::ParseError {
859                        expected: "labelLink end tag",
860                        value: "".to_string(),
861                    });
862                }
863                _ => {}
864            }
865
866            buf.clear();
867        }
868
869        Ok(LabelLink {
870            role,
871            locators,
872            arcs,
873            labels,
874        })
875    }
876
877    /// Parses a `referenceLink` element and its children.
878    fn parse_reference_link(&mut self, start: BytesStart) -> Result<ReferenceLink, XbrlError> {
879        // Extract the `xlink:role` attribute
880        let mut role = String::new();
881
882        for attribute in start.attributes() {
883            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
884                path: self.path.clone(),
885                position: self.reader.buffer_position(),
886                element: Some("referenceLink".to_string()),
887                source: err.into(),
888            })?;
889
890            if attribute.key.as_ref() == b"xlink:role" {
891                let value = attribute.decode_and_unescape_value(self.reader.decoder())?;
892                role = value.to_string();
893            }
894        }
895
896        let mut locators = Vec::new();
897        let mut arcs = Vec::new();
898        let mut references = Vec::new();
899        let mut buf = Vec::new();
900
901        loop {
902            match self.reader.read_event_into(&mut buf)? {
903                Event::Empty(event) | Event::Start(event) => match event.local_name().as_ref() {
904                    b"loc" => {
905                        let locator = self.parse_locator(&event)?;
906                        locators.push(locator);
907                    }
908                    b"referenceArc" => {
909                        let mut from = None;
910                        let mut to = None;
911
912                        for attribute in event.attributes() {
913                            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
914                                path: self.path.clone(),
915                                position: self.reader.buffer_position(),
916                                element: Some("referenceArc".to_string()),
917                                source: err.into(),
918                            })?;
919
920                            match attribute.key.as_ref() {
921                                b"xlink:from" => {
922                                    let value = attribute
923                                        .decode_and_unescape_value(self.reader.decoder())?;
924                                    from = Some(value.to_string());
925                                }
926                                b"xlink:to" => {
927                                    let value = attribute
928                                        .decode_and_unescape_value(self.reader.decoder())?;
929                                    to = Some(value.to_string());
930                                }
931                                _ => {}
932                            }
933                        }
934
935                        arcs.push(RawReferenceArc {
936                            from: from.ok_or_else(|| XbrlError::ParseError {
937                                expected: "xlink:from on referenceArc",
938                                value: "".to_string(),
939                            })?,
940                            to: to.ok_or_else(|| XbrlError::ParseError {
941                                expected: "xlink:to on referenceArc",
942                                value: "".to_string(),
943                            })?,
944                        });
945                    }
946                    b"reference" => {
947                        let mut label = None;
948                        let mut ref_role = None;
949
950                        for attribute in event.attributes() {
951                            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
952                                path: self.path.clone(),
953                                position: self.reader.buffer_position(),
954                                element: Some("reference".to_string()),
955                                source: err.into(),
956                            })?;
957
958                            match attribute.key.as_ref() {
959                                b"xlink:label" => {
960                                    let value = attribute
961                                        .decode_and_unescape_value(self.reader.decoder())?;
962                                    label = Some(value.to_string());
963                                }
964                                b"xlink:role" => {
965                                    let value = attribute
966                                        .decode_and_unescape_value(self.reader.decoder())?;
967                                    ref_role = Some(value.to_string());
968                                }
969                                _ => {}
970                            }
971                        }
972
973                        references.push(ReferenceResource {
974                            label: label.ok_or_else(|| XbrlError::ParseError {
975                                expected: "xlink:label on reference",
976                                value: "".to_string(),
977                            })?,
978                            role: ref_role,
979                        });
980                    }
981                    _ => {}
982                },
983                Event::End(event) => {
984                    if event.name() == start.name() {
985                        break;
986                    }
987                }
988                Event::Eof => {
989                    return Err(XbrlError::ParseError {
990                        expected: "referenceLink end tag",
991                        value: "".to_string(),
992                    });
993                }
994                _ => {}
995            }
996
997            buf.clear();
998        }
999
1000        Ok(ReferenceLink {
1001            role,
1002            locators,
1003            arcs,
1004            references,
1005        })
1006    }
1007
1008    /// Parses a `loc` element and extracts the `xlink:label` and `xlink:href`
1009    /// attributes.
1010    fn parse_locator(&mut self, event: &BytesStart) -> Result<Locator, XbrlError> {
1011        let mut label = None;
1012        let mut href = None;
1013
1014        for attribute in event.attributes() {
1015            let attribute = attribute.map_err(|err| XbrlError::XmlParse {
1016                path: self.path.clone(),
1017                position: self.reader.buffer_position(),
1018                element: Some("presentationLink".to_string()),
1019                source: err.into(),
1020            })?;
1021
1022            match attribute.key.as_ref() {
1023                b"xlink:label" => {
1024                    let value = attribute.decode_and_unescape_value(self.reader.decoder())?;
1025                    label = Some(value.to_string())
1026                }
1027                b"xlink:href" => {
1028                    let value = attribute.decode_and_unescape_value(self.reader.decoder())?;
1029                    href = Some(value.to_string())
1030                }
1031                _ => {}
1032            }
1033        }
1034        let locator = Locator {
1035            label: label.ok_or_else(|| XbrlError::ParseError {
1036                expected: "xlink:label on loc",
1037                value: "".to_string(),
1038            })?,
1039            href: href.ok_or_else(|| XbrlError::ParseError {
1040                expected: "xlink:href on loc",
1041                value: "".to_string(),
1042            })?,
1043        };
1044
1045        Ok(locator)
1046    }
1047}
1048
1049#[cfg(test)]
1050mod tests {
1051    use super::*;
1052    use assert_matches::assert_matches;
1053
1054    #[test]
1055    fn test_parse_missing_linkbase_root() {
1056        let xml = r#"<presentationLink
1057                                xmlns:link="http://www.xbrl.org/2003/linkbase"
1058                                xlink:type="extended"
1059                                xlink:role="http://example.com/role/balanceSheet">
1060                            </presentationLink>"#;
1061        let mut parser = LinkbaseParser::from_reader(xml.as_bytes());
1062        let mut linkbases = RawLinkbases::default();
1063
1064        let result = parser.parse_linkbase(&mut linkbases);
1065
1066        assert_matches!(result, Err(XbrlError::InvalidLinkbaseDocument { reason, .. }) if reason == "missing <linkbase> root element");
1067    }
1068
1069    #[test]
1070    fn test_parse_linkbase_presentation_link() {
1071        let xml = r#"<link:linkbase xmlns:link="http://www.xbrl.org/2003/linkbase">
1072                                <link:presentationLink
1073                                    xlink:type="extended"
1074                                    xlink:role="http://example.com/role/balanceSheet">
1075                                    <link:loc
1076                                        xlink:type="locator"
1077                                        xlink:href="taxonomy.xsd#Assets"
1078                                        xlink:label="loc_assets" />
1079                                    <link:loc
1080                                        xlink:type="locator"
1081                                        xlink:href="taxonomy.xsd#Cash"
1082                                        xlink:label="loc_cash" />
1083                                    <link:presentationArc
1084                                        xlink:type="arc"
1085                                        xlink:arcrole="http://www.xbrl.org/2003/arcrole/parent-child"
1086                                        xlink:from="loc_assets"
1087                                        xlink:to="loc_cash"
1088                                        order="1" />
1089                                </link:presentationLink>
1090                            </link:linkbase>"#;
1091        let mut parser = LinkbaseParser::from_reader(xml.as_bytes());
1092        let mut linkbases = RawLinkbases::default();
1093        parser.parse_linkbase(&mut linkbases).unwrap();
1094
1095        assert_eq!(linkbases.presentation_links.len(), 1);
1096        assert_eq!(linkbases.calculation_links.len(), 0);
1097        assert_eq!(linkbases.definition_links.len(), 0);
1098        assert_eq!(linkbases.label_links.len(), 0);
1099        assert_eq!(linkbases.reference_links.len(), 0);
1100
1101        let presentation_link = &linkbases.presentation_links[0];
1102        assert_eq!(
1103            presentation_link,
1104            &PresentationLink {
1105                role: "http://example.com/role/balanceSheet".to_string(),
1106                locators: vec![
1107                    Locator {
1108                        label: "loc_assets".to_string(),
1109                        href: "taxonomy.xsd#Assets".to_string(),
1110                    },
1111                    Locator {
1112                        label: "loc_cash".to_string(),
1113                        href: "taxonomy.xsd#Cash".to_string(),
1114                    },
1115                ],
1116                arcs: vec![RawPresentationArc {
1117                    from: "loc_assets".to_string(),
1118                    to: "loc_cash".to_string(),
1119                    order: Some(Decimal::new(1, 0)),
1120                    preferred_label: None,
1121                    arcrole: "http://www.xbrl.org/2003/arcrole/parent-child".into(),
1122                }],
1123            }
1124        );
1125    }
1126
1127    #[test]
1128    fn test_parse_linkbase_calculation_link() {
1129        let xml = r#"<link:linkbase xmlns:link="http://www.xbrl.org/2003/linkbase">
1130                            <link:calculationLink xlink:type="extended" xlink:role="">
1131                                <link:calculationArc
1132                                    xlink:type="arc"
1133                                    xlink:arcrole="http://www.xbrl.org/2003/arcrole/summation-item"
1134                                    xlink:from="loc_assets"
1135                                    xlink:to="loc_cash"
1136                                    weight="1"
1137                                    order="1" />
1138                            </link:calculationLink>
1139                        </link:linkbase>"#;
1140        let mut parser = LinkbaseParser::from_reader(xml.as_bytes());
1141        let mut linkbases = RawLinkbases::default();
1142        parser.parse_linkbase(&mut linkbases).unwrap();
1143
1144        assert_eq!(linkbases.presentation_links.len(), 0);
1145        assert_eq!(linkbases.calculation_links.len(), 1);
1146        assert_eq!(linkbases.definition_links.len(), 0);
1147        assert_eq!(linkbases.label_links.len(), 0);
1148        assert_eq!(linkbases.reference_links.len(), 0);
1149        let calculation_link = &linkbases.calculation_links[0];
1150        assert_eq!(
1151            calculation_link,
1152            &CalculationLink {
1153                role: "".to_string(),
1154                locators: vec![],
1155                arcs: vec![RawCalculationArc {
1156                    from: "loc_assets".to_string(),
1157                    to: "loc_cash".to_string(),
1158                    order: Some(Decimal::new(1, 0)),
1159                    weight: Decimal::new(1, 0),
1160                    arcrole: "http://www.xbrl.org/2003/arcrole/summation-item".into(),
1161                }],
1162            }
1163        );
1164    }
1165
1166    #[test]
1167    fn test_parse_linkbase_definition_link() {
1168        let xml = r#"<link:linkbase xmlns:link="http://www.xbrl.org/2003/linkbase">
1169                            <link:definitionLink xlink:type="extended" xlink:role="">
1170                                <link:definitionArc
1171                                    xlink:type="arc"
1172                                    xlink:arcrole="http://xbrl.org/int/dim/arcrole/domain-member"
1173                                    xlink:from="loc_domain"
1174                                    xlink:to="loc_member" />
1175                            </link:definitionLink>
1176                        </link:linkbase>"#;
1177        let mut parser = LinkbaseParser::from_reader(xml.as_bytes());
1178        let mut linkbases = RawLinkbases::default();
1179        parser.parse_linkbase(&mut linkbases).unwrap();
1180
1181        assert_eq!(linkbases.presentation_links.len(), 0);
1182        assert_eq!(linkbases.calculation_links.len(), 0);
1183        assert_eq!(linkbases.definition_links.len(), 1);
1184        assert_eq!(linkbases.label_links.len(), 0);
1185        assert_eq!(linkbases.reference_links.len(), 0);
1186        let definition_link = &linkbases.definition_links[0];
1187        assert_eq!(
1188            definition_link,
1189            &DefinitionLink {
1190                role: "".to_string(),
1191                locators: vec![],
1192                arcs: vec![RawDefinitionArc {
1193                    from: "loc_domain".to_string(),
1194                    to: "loc_member".to_string(),
1195                    arcrole: "http://xbrl.org/int/dim/arcrole/domain-member".into(),
1196                    order: None,
1197                }],
1198            }
1199        );
1200    }
1201
1202    #[test]
1203    fn test_parse_linkbase_label_link() {
1204        let xml = r#"<link:linkbase xmlns:link="http://www.xbrl.org/2003/linkbase">
1205                            <link:labelLink>
1206                                <link:loc
1207                                    xlink:type="locator"
1208                                    xlink:href="taxonomy.xsd#Assets"
1209                                    xlink:label="loc_assets" />
1210                                <link:label
1211                                    xlink:type="resource"
1212                                    xlink:label="lab_assets"
1213                                    xlink:role="http://www.xbrl.org/2003/role/label"
1214                                    xml:lang="en">
1215                                    Assets
1216                                </link:label>
1217                                <link:labelArc
1218                                    xlink:type="arc"
1219                                    xlink:from="loc_assets"
1220                                    xlink:to="lab_assets" />
1221                            </link:labelLink>
1222                        </link:linkbase>"#;
1223        let mut parser = LinkbaseParser::from_reader(xml.as_bytes());
1224        let mut linkbases = RawLinkbases::default();
1225        parser.parse_linkbase(&mut linkbases).unwrap();
1226
1227        assert_eq!(linkbases.presentation_links.len(), 0);
1228        assert_eq!(linkbases.calculation_links.len(), 0);
1229        assert_eq!(linkbases.definition_links.len(), 0);
1230        assert_eq!(linkbases.label_links.len(), 1);
1231        assert_eq!(linkbases.reference_links.len(), 0);
1232        let label_link = &linkbases.label_links[0];
1233        assert_eq!(
1234            label_link,
1235            &LabelLink {
1236                role: "".to_string(),
1237                locators: vec![Locator {
1238                    label: "loc_assets".to_string(),
1239                    href: "taxonomy.xsd#Assets".to_string(),
1240                }],
1241                arcs: vec![RawLabelArc {
1242                    from: "loc_assets".to_string(),
1243                    to: "lab_assets".to_string(),
1244                }],
1245                labels: vec![LabelResource {
1246                    label: "lab_assets".to_string(),
1247                    role: Some("http://www.xbrl.org/2003/role/label".to_string()),
1248                    lang: "en".to_string(),
1249                    text: "Assets".to_string(),
1250                }],
1251            }
1252        );
1253    }
1254
1255    #[test]
1256    fn test_parse_linkbase_reference_link() {
1257        let xml = r#"<link:linkbase xmlns:link="http://www.xbrl.org/2003/linkbase"
1258                            xmlns:xlink="http://www.w3.org/1999/xlink"
1259                            xmlns:my="http://example.com/my-taxonomy">
1260                            <!-- Extended link for references -->
1261                            <link:referenceLink xlink:type="extended" xlink:role="http://www.xbrl.org/2003/role/reference">
1262                                <!-- Locators point to concepts in the taxonomy -->
1263                                <link:loc xlink:type="locator" xlink:label="loc_assets" xlink:href="my-taxonomy.xsd#Assets" />
1264                                <link:loc xlink:type="locator" xlink:label="loc_cash" xlink:href="my-taxonomy.xsd#Cash" />
1265                                <!-- Arcs connect locators to resources -->
1266                                <link:referenceArc xlink:type="arc"
1267                                    xlink:from="loc_assets"
1268                                    xlink:to="ref_assets"
1269                                    order="1" />
1270                                <link:referenceArc xlink:type="arc"
1271                                    xlink:from="loc_cash"
1272                                    xlink:to="ref_cash"
1273                                    order="2" />
1274                                <!-- Resources provide textual references -->
1275                                <link:reference xlink:type="resource"
1276                                    xlink:label="ref_assets"
1277                                    xlink:role="http://www.xbrl.org/2003/role/statementRef">
1278                                    <link:content>Test content 1</link:content>
1279                                </link:reference>
1280                                <link:reference xlink:type="resource"
1281                                    xlink:label="ref_cash"
1282                                    xlink:role="http://www.xbrl.org/2003/role/statementRef">
1283                                    <link:content>Test content 2</link:content>
1284                                </link:reference>
1285                            </link:referenceLink>
1286                        </link:linkbase>"#;
1287        let mut parser = LinkbaseParser::from_reader(xml.as_bytes());
1288        let mut linkbases = RawLinkbases::default();
1289        parser.parse_linkbase(&mut linkbases).unwrap();
1290
1291        assert_eq!(linkbases.presentation_links.len(), 0);
1292        assert_eq!(linkbases.calculation_links.len(), 0);
1293        assert_eq!(linkbases.definition_links.len(), 0);
1294        assert_eq!(linkbases.label_links.len(), 0);
1295        assert_eq!(linkbases.reference_links.len(), 1);
1296        let reference_link = &linkbases.reference_links[0];
1297        assert_eq!(
1298            reference_link,
1299            &ReferenceLink {
1300                role: "http://www.xbrl.org/2003/role/reference".to_string(),
1301                locators: vec![
1302                    Locator {
1303                        label: "loc_assets".to_string(),
1304                        href: "my-taxonomy.xsd#Assets".to_string(),
1305                    },
1306                    Locator {
1307                        label: "loc_cash".to_string(),
1308                        href: "my-taxonomy.xsd#Cash".to_string(),
1309                    },
1310                ],
1311                arcs: vec![
1312                    RawReferenceArc {
1313                        from: "loc_assets".to_string(),
1314                        to: "ref_assets".to_string(),
1315                    },
1316                    RawReferenceArc {
1317                        from: "loc_cash".to_string(),
1318                        to: "ref_cash".to_string(),
1319                    },
1320                ],
1321                references: vec![
1322                    ReferenceResource {
1323                        label: "ref_assets".to_string(),
1324                        role: Some("http://www.xbrl.org/2003/role/statementRef".to_string()),
1325                    },
1326                    ReferenceResource {
1327                        label: "ref_cash".to_string(),
1328                        role: Some("http://www.xbrl.org/2003/role/statementRef".to_string()),
1329                    },
1330                ],
1331            }
1332        );
1333    }
1334}