1mod 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#[derive(Debug, Default)]
35pub struct InstanceDocument {
36 namespaces: HashMap<NamespacePrefix, NamespaceUri>,
39 schema_refs: Vec<String>,
41 role_refs: Vec<String>,
43 arcrole_refs: Vec<String>,
45 contexts: HashMap<ContextId, Context>,
47 units: HashMap<UnitId, Unit>,
49 facts: Vec<Fact>,
51 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 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 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 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 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 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 pub fn validate(&self, taxonomy: &TaxonomySet) -> ValidationResult {
233 validation::validate_all(self, taxonomy)
234 }
235
236 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 pub fn to_file(&self, path: &Path) -> Result<()> {
244 let file = File::create(path)?;
245 self.to_writer(file)?;
246 Ok(())
247 }
248
249 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 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 pub fn add_schema_ref(&mut self, href: String) {
268 self.schema_refs.push(href);
269 }
270
271 pub fn schema_refs(&self) -> &[String] {
273 &self.schema_refs
274 }
275
276 pub fn add_role_ref(&mut self, role_uri: String) {
278 self.role_refs.push(role_uri);
279 }
280
281 pub fn role_refs(&self) -> &[String] {
283 &self.role_refs
284 }
285
286 pub fn add_arcrole_ref(&mut self, arcrole_uri: String) {
288 self.arcrole_refs.push(arcrole_uri);
289 }
290
291 pub fn arcrole_refs(&self) -> &[String] {
293 &self.arcrole_refs
294 }
295
296 pub fn schema_ref_paths(&self) -> Vec<&str> {
305 self.schema_refs
306 .iter()
307 .map(|href| {
308 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 path.strip_prefix("/taxonomies/")
316 .or_else(|| path.strip_prefix("/"))
317 .unwrap_or(path)
318 })
319 .collect()
320 }
321
322 pub fn add_context(&mut self, context: Context) {
324 self.contexts.insert(context.id.clone(), context);
325 }
326
327 pub fn get_context(&self, id: &str) -> Option<&Context> {
329 self.contexts.get(id)
330 }
331
332 pub fn add_unit(&mut self, unit: Unit) {
334 self.units.insert(unit.id.clone(), unit);
335 }
336
337 pub fn get_unit(&self, id: &str) -> Option<&Unit> {
339 self.units.get(id)
340 }
341
342 pub fn add_fact(&mut self, fact: Fact) {
344 self.facts.push(fact);
345 }
346
347 pub fn facts(&self) -> &[Fact] {
349 &self.facts
350 }
351
352 pub fn facts_mut(&mut self) -> &mut [Fact] {
354 &mut self.facts
355 }
356
357 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 pub fn item_fact_count(&self) -> usize {
368 self.facts.iter().map(|fact| fact.count_items()).sum()
369 }
370
371 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 pub fn add_namespace(&mut self, prefix: NamespacePrefix, uri: NamespaceUri) {
389 self.namespaces.insert(prefix, uri);
390 }
391
392 pub fn get_namespace(&self, prefix: &str) -> Option<&str> {
394 self.namespaces.get(prefix).map(|s| s.as_str())
395 }
396
397 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 pub fn contexts(&self) -> &HashMap<ContextId, Context> {
412 &self.contexts
413 }
414
415 pub fn units(&self) -> &HashMap<UnitId, Unit> {
417 &self.units
418 }
419
420 #[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; }
447
448 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 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 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
569fn 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
599fn 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
618fn 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 let mut current = child_element;
635
636 loop {
637 let parent_substitution_group = ¤t.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}