Skip to main content

xbrl_rs/instance/
view.rs

1//! Document view built from the presentation linkbase.
2
3use super::fact::ItemFact;
4use crate::{ExpandedName, Label, PresentationArc, TaxonomySet};
5use std::{
6    cmp::Ordering,
7    collections::{HashMap, HashSet},
8};
9
10/// A hierarchical view of an XBRL document organised by presentation sections.
11#[derive(Debug)]
12pub struct DocumentView<'a> {
13    /// One section per extended link role found in the presentation linkbase.
14    pub sections: Vec<SectionView<'a>>,
15}
16
17impl<'a> DocumentView<'a> {
18    /// Build a document view from a flat facts slice and a taxonomy.
19    ///
20    /// `facts` is read once to map concept IDs to their positions; no
21    /// references into the slice are retained. The returned view borrows
22    /// only from `taxonomy`.
23    pub fn build(facts: &[&ItemFact], taxonomy: &'a TaxonomySet) -> Self {
24        build_view(facts, taxonomy)
25    }
26}
27
28/// One report section (extended link role) from the presentation linkbase.
29#[derive(Debug)]
30pub struct SectionView<'a> {
31    /// The extended link role URI identifying this section.
32    pub role: &'a str,
33    /// Root nodes of the presentation tree for this section.
34    pub nodes: Vec<TreeNode<'a>>,
35}
36
37/// A single node in the presentation hierarchy.
38#[derive(Debug)]
39pub struct TreeNode<'a> {
40    /// Concept name (e.g. `bs.ass`).
41    pub concept_name: &'a str,
42    /// All labels for this concept. The caller selects the desired language
43    /// and role (e.g. `terseLabel`, `label`).
44    pub labels: &'a [Label],
45    /// Depth in the tree; root nodes have depth 0.
46    pub depth: usize,
47    /// Indices into the `InstanceDocument::facts()` slice for facts whose concept
48    /// maps to this concept name.
49    ///
50    /// Storing indices rather than references means the view's lifetime is tied
51    /// only to the taxonomy, leaving the instance free to be mutably borrowed
52    /// while the view is alive.
53    pub fact_indices: Vec<usize>,
54    /// Child nodes, ordered by the presentation arc `order` attribute.
55    pub children: Vec<TreeNode<'a>>,
56}
57
58/// Build a [`DocumentView`] by walking the presentation linkbase and
59/// attaching instance facts and taxonomy labels to each node.
60///
61/// `facts` is borrowed for index-building only; no references into the slice
62/// are retained, so the returned `DocumentView<'a>` borrows only from
63/// `taxonomy`.
64pub fn build_view<'a>(facts: &[&ItemFact], taxonomy: &'a TaxonomySet) -> DocumentView<'a> {
65    // Index facts by their element ID → position in the facts slice.
66    let mut fact_index: HashMap<ExpandedName, Vec<usize>> = HashMap::new();
67
68    for (i, fact) in facts.iter().enumerate() {
69        fact_index
70            .entry(fact.concept_name().clone())
71            .or_default()
72            .push(i);
73    }
74
75    let roles = taxonomy
76        .presentations()
77        .iter()
78        .map(|(role, arcs)| (role.as_str(), arcs))
79        .collect::<Vec<_>>();
80
81    let mut sections = Vec::with_capacity(roles.len());
82
83    for (role, arcs) in roles {
84        let mut arc_index: HashMap<&'a ExpandedName, Vec<&'a PresentationArc>> = HashMap::new();
85
86        for arc in arcs {
87            arc_index.entry(&arc.from).or_default().push(arc);
88        }
89
90        // Sort children by `order` up front so `build_nodes` never needs to
91        // re-sort.
92        for children in arc_index.values_mut() {
93            children.sort_by(|a, b| match (a.order, b.order) {
94                (Some(x), Some(y)) => x.cmp(&y),
95                (Some(_), None) => Ordering::Less,
96                (None, Some(_)) => Ordering::Greater,
97                (None, None) => Ordering::Equal,
98            });
99        }
100
101        let roots = find_roots(arcs, &arc_index);
102        let mut visited: HashSet<&'a ExpandedName> = HashSet::new();
103        let nodes = roots
104            .iter()
105            .flat_map(|root_id| {
106                build_nodes(&arc_index, root_id, 0, taxonomy, &fact_index, &mut visited)
107            })
108            .collect();
109
110        sections.push(SectionView { role, nodes });
111    }
112
113    DocumentView { sections }
114}
115
116/// Find root concept IDs: those that appear as `from` but never as `to`.
117pub(super) fn find_roots<'a>(
118    arcs: &'a [PresentationArc],
119    arc_index: &HashMap<&'a ExpandedName, Vec<&'a PresentationArc>>,
120) -> Vec<&'a ExpandedName> {
121    let to_set: HashSet<&ExpandedName> = arcs.iter().map(|a| &a.to).collect();
122    let mut seen: HashSet<&ExpandedName> = HashSet::new();
123    let mut roots: Vec<&'a ExpandedName> = Vec::new();
124
125    for arc in arcs {
126        let from = &arc.from;
127
128        if !to_set.contains(from) && seen.insert(from) {
129            roots.push(from);
130        }
131    }
132
133    // Order roots by their minimum outgoing arc order using the pre-built index.
134    roots.sort_by(|a, b| {
135        let min_order = |id: &&ExpandedName| {
136            arc_index
137                .get(*id)
138                .and_then(|arcs| arcs.iter().filter_map(|a| a.order).min())
139        };
140        match (min_order(a), min_order(b)) {
141            (Some(x), Some(y)) => x.cmp(&y),
142            (Some(_), None) => Ordering::Less,
143            (None, Some(_)) => Ordering::Greater,
144            (None, None) => Ordering::Equal,
145        }
146    });
147
148    roots
149}
150
151/// Recursively build tree nodes for all children of `parent_id`.
152fn build_nodes<'a>(
153    arc_index: &HashMap<&'a ExpandedName, Vec<&'a PresentationArc>>,
154    parent_id: &'a ExpandedName,
155    depth: usize,
156    taxonomy: &'a TaxonomySet,
157    fact_index: &HashMap<ExpandedName, Vec<usize>>,
158    visited: &mut HashSet<&'a ExpandedName>,
159) -> Vec<TreeNode<'a>> {
160    if !visited.insert(parent_id) {
161        // The branch is skipped if a cycle is detected.
162        return Vec::new();
163    }
164
165    // Children are already sorted by `order`
166    let children_arcs = arc_index.get(parent_id).map(Vec::as_slice).unwrap_or(&[]);
167
168    let mut nodes = Vec::with_capacity(children_arcs.len());
169
170    for arc in children_arcs {
171        let child_id = &arc.to;
172        let labels = taxonomy.labels(child_id).unwrap_or_default();
173        let fact_indices = fact_index.get(child_id).cloned().unwrap_or_default();
174        let children = build_nodes(
175            arc_index,
176            child_id,
177            depth + 1,
178            taxonomy,
179            fact_index,
180            visited,
181        );
182
183        nodes.push(TreeNode {
184            concept_name: &child_id.local_name,
185            labels,
186            depth,
187            fact_indices,
188            children,
189        });
190    }
191
192    visited.remove(parent_id);
193
194    nodes
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use crate::{ItemFact, taxonomy::TaxonomySet};
201    use rust_decimal::Decimal;
202
203    fn create_taxonomy(
204        arcs: Vec<(String, PresentationArc)>,
205        labels: Vec<(ExpandedName, Label)>,
206    ) -> TaxonomySet {
207        let mut taxonomy = TaxonomySet::default();
208        for (role, arc) in arcs {
209            taxonomy.add_presentation_arc(role, arc);
210        }
211        for (concept_name, label) in labels {
212            taxonomy.add_label(concept_name, label);
213        }
214        taxonomy
215    }
216
217    #[test]
218    fn build_view_empty_taxonomy() {
219        let taxonomy = TaxonomySet::default();
220        let view = build_view(&[], &taxonomy);
221        assert!(view.sections.is_empty());
222    }
223
224    #[test]
225    fn build_view_single_section_with_hierarchy() {
226        let role = "http://example.com/role/bs".to_string();
227        let arcs = vec![
228            (
229                role.clone(),
230                PresentationArc {
231                    from: ExpandedName::new("http://example.com/namespace".into(), "root".into()),
232                    to: ExpandedName::new("http://example.com/namespace".into(), "child_a".into()),
233                    order: Some(Decimal::new(1, 0)),
234                    preferred_label: None,
235                    arcrole: "http://www.xbrl.org/2003/arcrole/parent-child".into(),
236                },
237            ),
238            (
239                role.clone(),
240                PresentationArc {
241                    from: ExpandedName::new("http://example.com/namespace".into(), "root".into()),
242                    to: ExpandedName::new("http://example.com/namespace".into(), "child_b".into()),
243                    order: Some(Decimal::new(2, 0)),
244                    preferred_label: None,
245                    arcrole: "http://www.xbrl.org/2003/arcrole/parent-child".into(),
246                },
247            ),
248            (
249                role.clone(),
250                PresentationArc {
251                    from: ExpandedName::new(
252                        "http://example.com/namespace".into(),
253                        "child_a".into(),
254                    ),
255                    to: ExpandedName::new(
256                        "http://example.com/namespace".into(),
257                        "grandchild".into(),
258                    ),
259                    order: Some(Decimal::new(1, 0)),
260                    preferred_label: None,
261                    arcrole: "http://www.xbrl.org/2003/arcrole/parent-child".into(),
262                },
263            ),
264        ];
265        let labels = vec![(
266            ExpandedName::new("http://example.com/namespace".into(), "child_a".into()),
267            Label {
268                role: "http://www.xbrl.org/2003/role/label".to_string(),
269                lang: "en".to_string(),
270                text: "Child A".to_string(),
271            },
272        )];
273        let taxonomy = create_taxonomy(arcs, labels);
274
275        // Use a QName without a prefix so concept_id() == "child_a" directly,
276        // matching the element ID used in the presentation arcs above.
277        let fact = ItemFact::new(
278            None,
279            ExpandedName::new("http://example.com/namespace".into(), "child_a".into()),
280            "ctx1".to_string(),
281            None,
282            "42".to_string(),
283            false,
284            None,
285            None,
286        );
287        let facts = vec![&fact];
288
289        let view = build_view(&facts, &taxonomy);
290
291        assert_eq!(view.sections.len(), 1);
292        let section = &view.sections[0];
293        assert_eq!(section.role, role);
294
295        // "root" is the root; its children are child_a and child_b
296        assert_eq!(section.nodes.len(), 2);
297
298        let node_a = &section.nodes[0];
299        assert_eq!(node_a.concept_name, "child_a");
300        assert_eq!(node_a.labels.len(), 1);
301        assert_eq!(node_a.labels[0].text, "Child A");
302        assert_eq!(node_a.labels[0].lang, "en");
303        assert_eq!(node_a.depth, 0);
304        assert_eq!(node_a.fact_indices.len(), 1);
305        assert_eq!(facts[node_a.fact_indices[0]].value(), "42");
306        assert_eq!(node_a.children.len(), 1);
307
308        let grandchild = &node_a.children[0];
309        assert_eq!(grandchild.concept_name, "grandchild");
310        assert_eq!(grandchild.depth, 1);
311        assert!(grandchild.labels.is_empty());
312
313        let node_b = &section.nodes[1];
314        assert_eq!(node_b.concept_name, "child_b");
315        assert!(node_b.fact_indices.is_empty());
316    }
317
318    #[test]
319    fn build_view_cycle_protection() {
320        let role = "http://example.com/role/cycle".to_string();
321        // a -> b -> a  (cycle)
322        let arcs = vec![
323            (
324                role.clone(),
325                PresentationArc {
326                    from: ExpandedName::new("http://example.com/namespace".into(), "a".into()),
327                    to: ExpandedName::new("http://example.com/namespace".into(), "b".into()),
328                    order: Some(Decimal::new(1, 0)),
329                    preferred_label: None,
330                    arcrole: "http://www.xbrl.org/2003/arcrole/parent-child".into(),
331                },
332            ),
333            (
334                role.clone(),
335                PresentationArc {
336                    from: ExpandedName::new("http://example.com/namespace".into(), "b".into()),
337                    to: ExpandedName::new("http://example.com/namespace".into(), "a".into()),
338                    order: Some(Decimal::new(1, 0)),
339                    preferred_label: None,
340                    arcrole: "http://www.xbrl.org/2003/arcrole/parent-child".into(),
341                },
342            ),
343        ];
344        let taxonomy = create_taxonomy(arcs, vec![]);
345        // Should not hang or panic.
346        let view = build_view(&[], &taxonomy);
347        assert_eq!(view.sections.len(), 1);
348    }
349
350    #[test]
351    fn sibling_order_respected() {
352        let role = "http://example.com/role/order".to_string();
353        let arcs = vec![
354            (
355                role.clone(),
356                PresentationArc {
357                    from: ExpandedName::new("http://example.com/namespace".into(), "root".into()),
358                    to: ExpandedName::new("http://example.com/namespace".into(), "b".into()),
359                    order: Some(Decimal::new(2, 0)),
360                    preferred_label: None,
361                    arcrole: "http://www.xbrl.org/2003/arcrole/parent-child".into(),
362                },
363            ),
364            (
365                role.clone(),
366                PresentationArc {
367                    from: ExpandedName::new("http://example.com/namespace".into(), "root".into()),
368                    to: ExpandedName::new("http://example.com/namespace".into(), "a".into()),
369                    order: Some(Decimal::new(1, 0)),
370                    preferred_label: None,
371                    arcrole: "http://www.xbrl.org/2003/arcrole/parent-child".into(),
372                },
373            ),
374        ];
375        let taxonomy = create_taxonomy(arcs, vec![]);
376        let view = build_view(&[], &taxonomy);
377
378        let section = &view.sections[0];
379        assert_eq!(section.nodes[0].concept_name, "a");
380        assert_eq!(section.nodes[1].concept_name, "b");
381    }
382}