Skip to main content

xbrl_rs/instance/
mod.rs

1//! XBRL instance document representation
2
3mod context;
4mod fact;
5mod footnote;
6mod parser;
7mod resolver;
8mod unit;
9mod view;
10mod writer;
11
12use crate::{
13    ExpandedName, NamespacePrefix, NamespaceUri, PresentationArc, TaxonomySet,
14    error::Result,
15    taxonomy::{Concept, PeriodType, TupleChild},
16    validation::{self, ValidationResult},
17};
18pub use context::{Context, ContextId, EntityIdentifier, Period};
19pub use fact::{Decimals, Fact, ItemFact, TupleFact};
20pub use footnote::{FootnoteArc, FootnoteLink, FootnoteLocator, FootnoteResource};
21pub use parser::InstanceParser;
22use quick_xml::{Reader, Writer};
23use std::{
24    cmp::Ordering,
25    collections::{HashMap, HashSet},
26    fs::File,
27    io,
28    path::Path,
29};
30pub use unit::{Unit, UnitId};
31pub use view::{DocumentView, SectionView, TreeNode};
32
33/// Represents a complete XBRL instance document
34#[derive(Debug, Default)]
35pub struct InstanceDocument {
36    /// Namespace prefixes used in the document (e.g. "xbrli" ->
37    /// "http://www.xbrl.org/2003/instance")
38    namespaces: HashMap<NamespacePrefix, NamespaceUri>,
39    /// Schema references (xlink:href values from link:schemaRef elements)
40    schema_refs: Vec<String>,
41    /// roleURI values from roleRef elements in the instance.
42    role_refs: Vec<String>,
43    /// arcroleURI values from arcroleRef elements in the instance.
44    arcrole_refs: Vec<String>,
45    /// All contexts in the instance
46    contexts: HashMap<ContextId, Context>,
47    /// All units in the instance
48    units: HashMap<UnitId, Unit>,
49    /// Top-level facts in the instance (item and tuple facts)
50    facts: Vec<Fact>,
51    /// Footnote links found in the instance.
52    footnote_links: Vec<FootnoteLink>,
53}
54
55impl InstanceDocument {
56    #[allow(clippy::too_many_arguments)]
57    pub fn new(
58        schema_refs: Vec<String>,
59        contexts: HashMap<ContextId, Context>,
60        units: HashMap<UnitId, Unit>,
61        facts: Vec<Fact>,
62        namespaces: HashMap<NamespacePrefix, NamespaceUri>,
63        footnote_links: Vec<FootnoteLink>,
64    ) -> Self {
65        Self {
66            schema_refs,
67            role_refs: Vec::new(),
68            arcrole_refs: Vec::new(),
69            contexts,
70            units,
71            facts,
72            namespaces,
73            footnote_links,
74        }
75    }
76
77    /// Create a new instance pre-wired to a known taxonomy.
78    ///
79    /// - Registers all schema refs and role refs from the taxonomy
80    /// - Adds both contexts and all provided units
81    /// - Pre-populates nil facts for concepts in the presentation linkbase,
82    ///   preserving tuple nesting derived directly from the presentation tree
83    /// - Assigns each fact the correct `unitRef` based on its XSD type:
84    ///   monetary → first currency unit, shares → first shares unit,
85    ///   other numeric → first pure unit, non-numeric → no unitRef
86    ///
87    /// Build the [`DocumentView`] once after this call, then fill values
88    /// in-place via [`set_fact_value`] without rebuilding the view.
89    pub fn from_taxonomy(
90        taxonomy: &TaxonomySet,
91        namespaces: HashMap<NamespacePrefix, NamespaceUri>,
92        instant_context: Context,
93        duration_context: Context,
94        units: &[Unit],
95    ) -> Self {
96        let mut instance = Self::default();
97
98        for (prefix, uri) in namespaces {
99            instance.add_namespace(prefix, uri);
100        }
101
102        for schema_url in taxonomy.schema_refs().keys() {
103            instance.add_schema_ref(schema_url.to_string());
104        }
105
106        let instant_context_ref = instant_context.id.clone();
107        let duration_context_ref = duration_context.id.clone();
108        instance.add_context(instant_context);
109        instance.add_context(duration_context);
110
111        for unit in units {
112            instance.add_unit(unit.clone());
113        }
114
115        // Walk the presentation tree in section order, depth-first within each section.
116        // The tree structure gives both the fact order and the tuple nesting directly,
117        // without needing to consult schema substitution groups.
118        let mut recursion_path: HashSet<ExpandedName> = HashSet::new();
119        let mut emitted_items: HashSet<ExpandedName> = HashSet::new();
120        let mut emitted_tuples: HashSet<ExpandedName> = HashSet::new();
121
122        for arcs in taxonomy.presentations().values() {
123            let mut arc_index: HashMap<&ExpandedName, Vec<&PresentationArc>> = HashMap::new();
124
125            for arc in arcs {
126                arc_index.entry(&arc.from).or_default().push(arc);
127            }
128
129            for children in arc_index.values_mut() {
130                children.sort_by(|a, b| match (a.order, b.order) {
131                    (Some(x), Some(y)) => x.cmp(&y),
132                    (Some(_), None) => Ordering::Less,
133                    (None, Some(_)) => Ordering::Greater,
134                    (None, None) => Ordering::Equal,
135                });
136            }
137
138            let roots = view::find_roots(arcs, &arc_index);
139            let mut seeded_nodes: HashSet<&ExpandedName> = HashSet::new();
140
141            for root_id in roots {
142                seeded_nodes.insert(root_id);
143                let mut hoisted: Vec<Fact> = Vec::new();
144                Self::populate_from_tree(
145                    &arc_index,
146                    root_id,
147                    taxonomy,
148                    &instant_context_ref,
149                    &duration_context_ref,
150                    units,
151                    &mut instance.facts,
152                    &mut emitted_items,
153                    &mut emitted_tuples,
154                    &mut recursion_path,
155                    None,
156                    &mut hoisted,
157                );
158                instance.facts.extend(hoisted);
159            }
160
161            let mut remaining_nodes = arcs
162                .iter()
163                .flat_map(|arc| [&arc.from, &arc.to])
164                .filter(|concept_name| !seeded_nodes.contains(concept_name))
165                .collect::<Vec<_>>();
166            remaining_nodes.sort_unstable();
167            remaining_nodes.dedup();
168
169            for concept_name in remaining_nodes {
170                let mut hoisted: Vec<Fact> = Vec::new();
171                Self::populate_from_tree(
172                    &arc_index,
173                    concept_name,
174                    taxonomy,
175                    &instant_context_ref,
176                    &duration_context_ref,
177                    units,
178                    &mut instance.facts,
179                    &mut emitted_items,
180                    &mut emitted_tuples,
181                    &mut recursion_path,
182                    None,
183                    &mut hoisted,
184                );
185                instance.facts.extend(hoisted);
186            }
187        }
188
189        instance
190    }
191
192    /// Parse an XBRL instance document from the file at the given path.
193    ///
194    /// Automatically extracts the `<xbrli:xbrl>` element if the input
195    /// contains a wrapper around it.
196    pub fn from_file(path: &Path) -> Result<Self> {
197        let mut parser = InstanceParser::from_file(path)?;
198        let instance = parser.parse_instance()?;
199        let doc = resolver::resolve_instance(instance)?;
200        Ok(doc)
201    }
202
203    /// Parse an XBRL instance document from the reader.
204    ///
205    /// Automatically extracts the `<xbrli:xbrl>` element if the input
206    /// contains a wrapper around it.
207    pub fn from_reader<R>(reader: R) -> Result<Self>
208    where
209        R: io::BufRead,
210    {
211        let mut parser = InstanceParser::from_reader(reader);
212        let instance = parser.parse_instance()?;
213        let doc = resolver::resolve_instance(instance)?;
214        Ok(doc)
215    }
216
217    /// Parse an XBRL instance document from the XML reader.
218    ///
219    /// Automatically extracts the `<xbrli:xbrl>` element if the input
220    /// contains a wrapper around it.
221    pub fn from_xml_reader<R>(reader: Reader<R>) -> Result<Self>
222    where
223        R: io::BufRead,
224    {
225        let mut parser = InstanceParser::new(reader);
226        let instance = parser.parse_instance()?;
227        let doc = resolver::resolve_instance(instance)?;
228        Ok(doc)
229    }
230
231    /// Validate this instance against a taxonomy.
232    pub fn validate(&self, taxonomy: &TaxonomySet) -> ValidationResult {
233        validation::validate_all(self, taxonomy)
234    }
235
236    /// Convenience wrapper for [`DocumentView::build`] using this instance's item facts.
237    pub fn view<'a>(&self, taxonomy: &'a TaxonomySet) -> DocumentView<'a> {
238        let item_facts = self.item_facts();
239        DocumentView::build(&item_facts, taxonomy)
240    }
241
242    /// Serialize this instance to an XML file at the given path.
243    pub fn to_file(&self, path: &Path) -> Result<()> {
244        let file = File::create(path)?;
245        self.to_writer(file)?;
246        Ok(())
247    }
248
249    /// Serialize this instance to an XBRL XML document using a writer.
250    pub fn to_writer<W>(&self, writer: W) -> Result<()>
251    where
252        W: io::Write,
253    {
254        let mut writer = Writer::new(writer);
255        writer::write_xml(&mut writer, self)
256    }
257
258    /// Serialize this instance to an XBRL XML document using an XML writer.
259    pub fn to_xml_writer<W>(&self, writer: &mut Writer<W>) -> Result<()>
260    where
261        W: io::Write,
262    {
263        writer::write_xml(writer, self)
264    }
265
266    /// Add a schema reference (xlink:href from a link:schemaRef element)
267    pub fn add_schema_ref(&mut self, href: String) {
268        self.schema_refs.push(href);
269    }
270
271    /// Get all schema references declared in the instance document.
272    pub fn schema_refs(&self) -> &[String] {
273        &self.schema_refs
274    }
275
276    /// Add a role reference URI from a roleRef element.
277    pub fn add_role_ref(&mut self, role_uri: String) {
278        self.role_refs.push(role_uri);
279    }
280
281    /// Get all role reference URIs declared in the instance document.
282    pub fn role_refs(&self) -> &[String] {
283        &self.role_refs
284    }
285
286    /// Add an arcrole reference URI from an arcroleRef element.
287    pub fn add_arcrole_ref(&mut self, arcrole_uri: String) {
288        self.arcrole_refs.push(arcrole_uri);
289    }
290
291    /// Get all arcrole reference URIs declared in the instance document.
292    pub fn arcrole_refs(&self) -> &[String] {
293        &self.arcrole_refs
294    }
295
296    /// Extract relative path suffixes from schema reference URLs.
297    ///
298    /// Strips the URL scheme, host, and leading `/taxonomies/` segment to
299    /// produce paths suitable for joining with a local taxonomy directory.
300    ///
301    /// For example:
302    /// `http://www.xbrl.de/taxonomies/de-gcd-2020-04-01/de-gcd-2020-04-01-shell.xsd`
303    /// becomes `de-gcd-2020-04-01/de-gcd-2020-04-01-shell.xsd`.
304    pub fn schema_ref_paths(&self) -> Vec<&str> {
305        self.schema_refs
306            .iter()
307            .map(|href| {
308                // Find the path portion after "://" + host
309                let path = href
310                    .find("://")
311                    .and_then(|i| href[i + 3..].find('/'))
312                    .map(|i| &href[href.find("://").unwrap() + 3 + i..])
313                    .unwrap_or(href);
314                // Strip leading "/taxonomies/" if present
315                path.strip_prefix("/taxonomies/")
316                    .or_else(|| path.strip_prefix("/"))
317                    .unwrap_or(path)
318            })
319            .collect()
320    }
321
322    /// Add a context to the instance
323    pub fn add_context(&mut self, context: Context) {
324        self.contexts.insert(context.id.clone(), context);
325    }
326
327    /// Get a context by ID
328    pub fn get_context(&self, id: &str) -> Option<&Context> {
329        self.contexts.get(id)
330    }
331
332    /// Add a unit to the instance
333    pub fn add_unit(&mut self, unit: Unit) {
334        self.units.insert(unit.id.clone(), unit);
335    }
336
337    /// Get a unit by ID
338    pub fn get_unit(&self, id: &str) -> Option<&Unit> {
339        self.units.get(id)
340    }
341
342    /// Add a fact to the instance
343    pub fn add_fact(&mut self, fact: Fact) {
344        self.facts.push(fact);
345    }
346
347    /// Get all top-level facts.
348    pub fn facts(&self) -> &[Fact] {
349        &self.facts
350    }
351
352    /// Get all top-level facts mutably.
353    pub fn facts_mut(&mut self) -> &mut [Fact] {
354        &mut self.facts
355    }
356
357    /// Get all item facts in depth-first order.
358    pub fn item_facts(&self) -> Vec<&ItemFact> {
359        let mut out = Vec::new();
360        for fact in &self.facts {
361            fact.walk_items(&mut out);
362        }
363        out
364    }
365
366    /// Number of item facts in the instance (including nested tuple descendants).
367    pub fn item_fact_count(&self) -> usize {
368        self.facts.iter().map(|fact| fact.count_items()).sum()
369    }
370
371    /// Set the value of a fact by its index (from [`DocumentView`] fact_indices).
372    /// Clears nil status.
373    ///
374    /// # Panics
375    /// Panics if `index` is out of bounds.
376    pub fn set_fact_value(&mut self, index: usize, value: String) {
377        let mut current_index = 0usize;
378        for fact in &mut self.facts {
379            if Self::set_item_value_by_index(fact, index, &value, &mut current_index) {
380                return;
381            }
382        }
383
384        panic!("fact index out of bounds: {index}");
385    }
386
387    /// Add a namespace prefix mapping
388    pub fn add_namespace(&mut self, prefix: NamespacePrefix, uri: NamespaceUri) {
389        self.namespaces.insert(prefix, uri);
390    }
391
392    /// Get namespace URI for a prefix
393    pub fn get_namespace(&self, prefix: &str) -> Option<&str> {
394        self.namespaces.get(prefix).map(|s| s.as_str())
395    }
396
397    /// Get all namespace prefix mappings
398    pub fn namespaces(&self) -> &HashMap<NamespacePrefix, NamespaceUri> {
399        &self.namespaces
400    }
401
402    pub fn add_footnote_link(&mut self, footnote_link: FootnoteLink) {
403        self.footnote_links.push(footnote_link);
404    }
405
406    pub fn footnote_links(&self) -> &[FootnoteLink] {
407        &self.footnote_links
408    }
409
410    /// Get all contexts
411    pub fn contexts(&self) -> &HashMap<ContextId, Context> {
412        &self.contexts
413    }
414
415    /// Get all units
416    pub fn units(&self) -> &HashMap<UnitId, Unit> {
417        &self.units
418    }
419
420    /// Recursively walk one node of the presentation tree and emit facts.
421    ///
422    /// - Concrete tuple → push a [`TupleFact`] and recurse into its children.
423    /// - Concrete item  → push an [`ItemFact`] (nil placeholder).  If the item
424    ///   is not a valid schema child of the enclosing tuple (per its
425    ///   `xs:complexType` content model) it is pushed to `hoisted` instead,
426    ///   which `from_taxonomy` appends to the top-level facts after all sections
427    ///   have been traversed.
428    /// - Abstract / grouping → recurse into children at the same level.
429    #[allow(clippy::too_many_arguments)]
430    fn populate_from_tree(
431        arc_index: &HashMap<&ExpandedName, Vec<&PresentationArc>>,
432        concept_name: &ExpandedName,
433        taxonomy: &TaxonomySet,
434        instant_ctx: &ContextId,
435        duration_ctx: &ContextId,
436        units: &[Unit],
437        facts: &mut Vec<Fact>,
438        emitted_items: &mut HashSet<ExpandedName>,
439        emitted_tuples: &mut HashSet<ExpandedName>,
440        recursion_path: &mut HashSet<ExpandedName>,
441        parent_tuple_element: Option<&Concept>,
442        hoisted: &mut Vec<Fact>,
443    ) {
444        if !recursion_path.insert(concept_name.clone()) {
445            return; // cycle guard within current recursion branch
446        }
447
448        // Children are already sorted by `order`.
449        let children = arc_index
450            .get(concept_name)
451            .map(Vec::as_slice)
452            .unwrap_or(&[]);
453
454        if let Some(concept) = taxonomy.find_concept(concept_name) {
455            if concept.is_tuple() && !concept.is_abstract {
456                if emitted_tuples.insert(concept_name.clone()) {
457                    facts.push(Fact::Tuple(TupleFact::new(concept.name.clone())));
458
459                    let tuple_children = match facts.last_mut() {
460                        Some(Fact::Tuple(tuple)) => tuple.children_mut(),
461                        _ => unreachable!(),
462                    };
463
464                    for arc in children {
465                        Self::populate_from_tree(
466                            arc_index,
467                            &arc.to,
468                            taxonomy,
469                            instant_ctx,
470                            duration_ctx,
471                            units,
472                            tuple_children,
473                            emitted_items,
474                            emitted_tuples,
475                            recursion_path,
476                            Some(concept),
477                            hoisted,
478                        );
479                    }
480                }
481                recursion_path.remove(concept_name);
482                return;
483            }
484
485            if !concept.is_abstract
486                && let Some(ref period_type) = concept.period_type
487            {
488                let context_ref = match period_type {
489                    PeriodType::Duration => duration_ctx,
490                    PeriodType::Instant => instant_ctx,
491                };
492
493                if emitted_items.insert(concept_name.clone()) {
494                    let mut fact = ItemFact::new(
495                        None,
496                        concept.name.clone(),
497                        context_ref.to_string(),
498                        unit_ref_for_concept(concept, units),
499                        String::new(),
500                        true,
501                        None,
502                        None,
503                    );
504                    fact.set_nil(true);
505
506                    // Items not allowed by the tuple's content model are hoisted to
507                    // the top level so they still appear in the generated template.
508                    if let Some(parent_el) = parent_tuple_element
509                        && !item_allowed_in_tuple(parent_el, concept, taxonomy)
510                    {
511                        hoisted.push(Fact::Item(fact));
512                    } else {
513                        facts.push(Fact::Item(fact));
514                    }
515                }
516            }
517        }
518
519        // Recurse children at the same level for non-structural presentation parents.
520        for arc in children {
521            Self::populate_from_tree(
522                arc_index,
523                &arc.to,
524                taxonomy,
525                instant_ctx,
526                duration_ctx,
527                units,
528                facts,
529                emitted_items,
530                emitted_tuples,
531                recursion_path,
532                parent_tuple_element,
533                hoisted,
534            );
535        }
536
537        recursion_path.remove(concept_name);
538    }
539
540    fn set_item_value_by_index(
541        fact: &mut Fact,
542        target_index: usize,
543        value: &str,
544        current_index: &mut usize,
545    ) -> bool {
546        match fact {
547            Fact::Item(item) => {
548                if *current_index == target_index {
549                    item.set_value(value.to_owned());
550                    item.set_nil(false);
551                    true
552                } else {
553                    *current_index += 1;
554                    false
555                }
556            }
557            Fact::Tuple(tuple) => {
558                for child in tuple.children_mut() {
559                    if Self::set_item_value_by_index(child, target_index, value, current_index) {
560                        return true;
561                    }
562                }
563                false
564            }
565        }
566    }
567}
568
569/// Determine the correct `unitRef` string for an element based on its XSD type.
570///
571/// - Monetary items  → first currency unit (`is_currency()`)
572/// - Shares items    → first shares unit (`is_shares()`)
573/// - Other numeric   → first pure unit (`is_pure()`)
574/// - Non-numeric     → `None` (unitRef forbidden by the XBRL spec)
575fn unit_ref_for_concept(concept: &Concept, units: &[Unit]) -> Option<String> {
576    let type_name = &concept.data_type;
577
578    if type_name.is_monetary() {
579        return units
580            .iter()
581            .find(|u| u.is_currency())
582            .map(|u| u.id.to_string());
583    }
584
585    if type_name.is_shares() {
586        return units
587            .iter()
588            .find(|u| u.is_shares())
589            .map(|u| u.id.to_string());
590    }
591
592    if type_name.is_numeric() {
593        return units.iter().find(|u| u.is_pure()).map(|u| u.id.to_string());
594    }
595
596    None
597}
598
599/// Returns `true` if `child_element` is a valid schema child of `parent_element`.
600///
601/// A child is allowed when `parent_element.tuple_children` is empty (no explicit
602/// content model) or when the child's element name or substitution-group ancestry
603/// matches one of the declared `xs:element ref` entries.
604fn item_allowed_in_tuple(
605    parent_element: &Concept,
606    child_element: &Concept,
607    taxonomy: &TaxonomySet,
608) -> bool {
609    if parent_element.tuple_children.is_empty() {
610        return true;
611    }
612    parent_element
613        .tuple_children
614        .iter()
615        .any(|child_ref| matches_tuple_child_ref(child_ref, child_element, taxonomy))
616}
617
618/// Returns `true` if `child_element` satisfies the `child_ref` constraint, either
619/// by a direct name match or via its substitution-group ancestry chain.
620fn matches_tuple_child_ref(
621    child_ref: &TupleChild,
622    child_element: &Concept,
623    taxonomy: &TaxonomySet,
624) -> bool {
625    let allowed_local = &child_ref.name.local_name;
626
627    if &child_element.name.local_name == allowed_local {
628        return true;
629    }
630
631    // Walk the substitution group ancestry: if the child's substitution group
632    // (or any ancestor in the chain) matches the declared child ref, the
633    // element is a valid substitute.
634    let mut current = child_element;
635
636    loop {
637        let parent_substitution_group = &current.substitution_group.original;
638
639        if &parent_substitution_group.local_name == allowed_local {
640            return true;
641        }
642
643        match taxonomy.find_concept(parent_substitution_group) {
644            Some(parent) => current = parent,
645            None => break,
646        }
647    }
648
649    false
650}
651
652#[cfg(test)]
653mod tests {
654    use super::{InstanceDocument, TaxonomySet};
655
656    #[test]
657    fn from_xml_parses_basic_instance() {
658        let xml = r#"
659            <xbrli:xbrl
660                xmlns:xbrli="http://www.xbrl.org/2003/instance"
661                xmlns:link="http://www.xbrl.org/2003/linkbase"
662                xmlns:xlink="http://www.w3.org/1999/xlink">
663                <link:schemaRef
664                    xlink:type="simple"
665                    xlink:href="http://www.xbrl.de/taxonomies/de-gcd-2020-04-01/de-gcd-2020-04-01-shell.xsd"/>
666            </xbrli:xbrl>
667        "#;
668
669        let instance =
670            InstanceDocument::from_reader(xml.as_bytes()).expect("instance should parse");
671
672        assert_eq!(instance.schema_refs().len(), 1);
673        assert!(instance.contexts().is_empty());
674        assert!(instance.units().is_empty());
675        assert!(instance.facts().is_empty());
676    }
677
678    #[test]
679    fn validate_reports_duplicate_role_refs() {
680        let taxonomy = TaxonomySet::default();
681        let mut instance = InstanceDocument::default();
682        let role_uri = "http://www.xbrl.org/2003/role/link".to_string();
683
684        instance.add_role_ref(role_uri.clone());
685        instance.add_role_ref(role_uri);
686
687        let result = instance.validate(&taxonomy);
688
689        assert!(!result.is_valid());
690        assert!(
691            result
692                .errors()
693                .iter()
694                .any(|message| message.code == "spec.duplicate_role_ref")
695        );
696    }
697
698    #[test]
699    fn validate_reports_duplicate_arcrole_refs() {
700        let taxonomy = TaxonomySet::default();
701        let mut instance = InstanceDocument::default();
702        let arcrole_uri = "http://www.xbrl.org/2003/arcrole/fact-footnote".to_string();
703
704        instance.add_arcrole_ref(arcrole_uri.clone());
705        instance.add_arcrole_ref(arcrole_uri);
706
707        let result = instance.validate(&taxonomy);
708
709        assert!(!result.is_valid());
710        assert!(
711            result
712                .errors()
713                .iter()
714                .any(|message| message.code == "spec.duplicate_arcrole_ref")
715        );
716    }
717
718    #[test]
719    fn validate_accepts_unique_refs() {
720        let taxonomy = TaxonomySet::default();
721        let mut instance = InstanceDocument::default();
722
723        instance.add_role_ref("http://www.xbrl.org/2003/role/link".to_string());
724        instance.add_arcrole_ref("http://www.xbrl.org/2003/arcrole/fact-footnote".to_string());
725
726        let result = instance.validate(&taxonomy);
727
728        assert!(
729            result.is_valid(),
730            "unexpected errors: {:#?}",
731            result.errors()
732        );
733        assert!(result.errors().is_empty());
734    }
735
736    #[test]
737    fn from_xml_parses_role_and_arcrole_refs() {
738        let xml = r#"
739            <xbrli:xbrl
740                xmlns:xbrli="http://www.xbrl.org/2003/instance"
741                xmlns:link="http://www.xbrl.org/2003/linkbase"
742                xmlns:xlink="http://www.w3.org/1999/xlink">
743                <link:roleRef
744                    roleURI="http://www.xbrl.org/2003/role/link"
745                    xlink:type="simple"
746                    xlink:href="dummy.xsd#role_link"/>
747                <link:arcroleRef
748                    arcroleURI="http://www.xbrl.org/2003/arcrole/fact-footnote"
749                    xlink:type="simple"
750                    xlink:href="dummy.xsd#arcrole_fact_footnote"/>
751            </xbrli:xbrl>
752        "#;
753
754        let instance =
755            InstanceDocument::from_reader(xml.as_bytes()).expect("instance should parse");
756
757        assert_eq!(instance.role_refs(), ["http://www.xbrl.org/2003/role/link"]);
758        assert_eq!(
759            instance.arcrole_refs(),
760            ["http://www.xbrl.org/2003/arcrole/fact-footnote"]
761        );
762    }
763
764    #[test]
765    fn validate_reports_both_duplicate_role_and_arcrole_refs() {
766        let taxonomy = TaxonomySet::default();
767        let mut instance = InstanceDocument::default();
768
769        instance.add_role_ref("http://www.xbrl.org/2003/role/link".to_string());
770        instance.add_role_ref("http://www.xbrl.org/2003/role/link".to_string());
771        instance.add_arcrole_ref("http://www.xbrl.org/2003/arcrole/fact-footnote".to_string());
772        instance.add_arcrole_ref("http://www.xbrl.org/2003/arcrole/fact-footnote".to_string());
773
774        let result = instance.validate(&taxonomy);
775
776        assert!(!result.is_valid());
777        assert!(
778            result
779                .errors()
780                .iter()
781                .any(|message| message.code == "spec.duplicate_role_ref")
782        );
783        assert!(
784            result
785                .errors()
786                .iter()
787                .any(|message| message.code == "spec.duplicate_arcrole_ref")
788        );
789    }
790}