1use super::{
4 Concept, GroupParticle, Label, Occurrence, Particle, PresentationArc, Reference, TaxonomySet,
5};
6use crate::ExpandedName;
7use rust_decimal::Decimal;
8use std::{
9 cmp::Ordering,
10 collections::{HashMap, HashSet},
11};
12
13#[derive(Debug)]
15pub struct TaxonomyView<'a> {
16 pub sections: Vec<TaxonomySectionView<'a>>,
18}
19
20impl<'a> TaxonomyView<'a> {
21 pub fn build(taxonomy: &'a TaxonomySet) -> Self {
23 build_taxonomy_view(taxonomy)
24 }
25
26 pub fn build_for_role(taxonomy: &'a TaxonomySet, role: &'a str) -> Option<Self> {
28 taxonomy.presentation_arcs(role).map(|arcs| TaxonomyView {
29 sections: vec![build_section(role, arcs, taxonomy)],
30 })
31 }
32}
33
34#[derive(Debug)]
36pub struct TaxonomySectionView<'a> {
37 pub role: &'a str,
39 pub nodes: Vec<TaxonomyTreeNode<'a>>,
41}
42
43#[derive(Debug)]
45pub struct TaxonomyTreeNode<'a> {
46 pub concept: &'a Concept,
48 pub labels: &'a [Label],
50 pub depth: usize,
52 pub children: Vec<TaxonomyTreeNode<'a>>,
54}
55
56#[derive(Debug)]
58pub struct ConceptView<'a> {
59 pub concept: &'a Concept,
61 pub labels: &'a [Label],
63 pub references: &'a [Reference],
65 pub parent_tuple: Option<&'a Concept>,
67 pub tuple_ancestors: Vec<&'a Concept>,
69 pub presentation_parents: Vec<PresentationRelationView<'a>>,
71 pub presentation_children: Vec<PresentationRelationView<'a>>,
73 pub tuple_content: Option<TupleParticleView<'a>>,
75}
76
77impl<'a> ConceptView<'a> {
78 pub fn build(concept: &'a Concept, taxonomy: &'a TaxonomySet) -> Self {
80 build_concept_view(concept, taxonomy)
81 }
82
83 pub fn build_from_name(concept_name: &ExpandedName, taxonomy: &'a TaxonomySet) -> Option<Self> {
85 taxonomy
86 .find_concept(concept_name)
87 .map(|concept| build_concept_view(concept, taxonomy))
88 }
89
90 pub fn build_from_id(concept_id: &str, taxonomy: &'a TaxonomySet) -> Option<Self> {
92 taxonomy
93 .find_concept_by_id(concept_id)
94 .map(|concept| build_concept_view(concept, taxonomy))
95 }
96}
97
98#[derive(Debug)]
100pub struct PresentationRelationView<'a> {
101 pub role: &'a str,
103 pub concept_name: &'a ExpandedName,
105 pub concept: Option<&'a Concept>,
107 pub order: Option<Decimal>,
109 pub preferred_label: Option<&'a str>,
111}
112
113#[derive(Debug)]
115pub enum TupleParticleView<'a> {
116 Element {
118 element: TupleElementView<'a>,
120 occurs: &'a Occurrence,
122 },
123 Sequence {
125 children: Vec<TupleParticleView<'a>>,
127 occurs: &'a Occurrence,
129 },
130 Choice {
132 children: Vec<TupleParticleView<'a>>,
134 occurs: &'a Occurrence,
136 },
137 GroupRef {
139 name: &'a str,
141 occurs: &'a Occurrence,
143 },
144 GroupDef {
146 name: Option<&'a str>,
148 particle: Box<TupleParticleView<'a>>,
150 occurs: &'a Occurrence,
152 },
153}
154
155#[derive(Debug)]
157pub struct TupleElementView<'a> {
158 pub local_name: &'a str,
160 pub concept: Option<&'a Concept>,
162}
163
164fn build_taxonomy_view<'a>(taxonomy: &'a TaxonomySet) -> TaxonomyView<'a> {
165 let sections = taxonomy
166 .presentations()
167 .iter()
168 .map(|(role, arcs)| build_section(role.as_str(), arcs, taxonomy))
169 .collect();
170
171 TaxonomyView { sections }
172}
173
174fn build_section<'a>(
175 role: &'a str,
176 arcs: &'a [PresentationArc],
177 taxonomy: &'a TaxonomySet,
178) -> TaxonomySectionView<'a> {
179 let concept_index = taxonomy
180 .concepts()
181 .map(|concept| (&concept.name, concept))
182 .collect::<HashMap<_, _>>();
183
184 let mut arc_index: HashMap<&'a ExpandedName, Vec<&'a PresentationArc>> = HashMap::new();
185 for arc in arcs {
186 arc_index.entry(&arc.from).or_default().push(arc);
187 }
188
189 for children in arc_index.values_mut() {
190 children.sort_by(|a, b| match (a.order, b.order) {
191 (Some(x), Some(y)) => x.cmp(&y),
192 (Some(_), None) => Ordering::Less,
193 (None, Some(_)) => Ordering::Greater,
194 (None, None) => Ordering::Equal,
195 });
196 }
197
198 let roots = find_roots(arcs, &arc_index);
199 let mut visited: HashSet<&'a ExpandedName> = HashSet::new();
200 let mut nodes = Vec::with_capacity(roots.len());
201
202 for root_id in roots {
203 if let Some(node) = build_node(
204 &concept_index,
205 &arc_index,
206 root_id,
207 0,
208 taxonomy,
209 &mut visited,
210 ) {
211 nodes.push(node);
212 }
213 }
214
215 TaxonomySectionView { role, nodes }
216}
217
218fn find_roots<'a>(
220 arcs: &'a [PresentationArc],
221 arc_index: &HashMap<&'a ExpandedName, Vec<&'a PresentationArc>>,
222) -> Vec<&'a ExpandedName> {
223 let to_set: HashSet<&ExpandedName> = arcs.iter().map(|arc| &arc.to).collect();
224 let mut roots: Vec<&'a ExpandedName> = arc_index
225 .keys()
226 .copied()
227 .filter(|from| !to_set.contains(*from))
228 .collect();
229
230 roots.sort_by(|a, b| {
231 let min_order = |id: &&ExpandedName| {
232 arc_index
233 .get(*id)
234 .and_then(|children| children.iter().filter_map(|arc| arc.order).min())
235 };
236 match (min_order(a), min_order(b)) {
237 (Some(x), Some(y)) => x.cmp(&y),
238 (Some(_), None) => Ordering::Less,
239 (None, Some(_)) => Ordering::Greater,
240 (None, None) => Ordering::Equal,
241 }
242 });
243
244 roots
245}
246
247fn build_node<'a>(
248 concept_index: &HashMap<&'a ExpandedName, &'a Concept>,
249 arc_index: &HashMap<&'a ExpandedName, Vec<&'a PresentationArc>>,
250 concept_id: &'a ExpandedName,
251 depth: usize,
252 taxonomy: &'a TaxonomySet,
253 visited: &mut HashSet<&'a ExpandedName>,
254) -> Option<TaxonomyTreeNode<'a>> {
255 if !visited.insert(concept_id) {
256 return None;
257 }
258
259 let concept = concept_index.get(concept_id).copied()?;
260 let labels = taxonomy.labels(concept_id).unwrap_or(&[]);
261
262 let child_arcs = arc_index.get(concept_id).map(Vec::as_slice).unwrap_or(&[]);
263 let mut children = Vec::with_capacity(child_arcs.len());
264
265 for arc in child_arcs {
266 if let Some(child_node) = build_node(
267 concept_index,
268 arc_index,
269 &arc.to,
270 depth + 1,
271 taxonomy,
272 visited,
273 ) {
274 children.push(child_node);
275 }
276 }
277
278 visited.remove(concept_id);
279
280 Some(TaxonomyTreeNode {
281 concept,
282 labels,
283 depth,
284 children,
285 })
286}
287
288fn build_concept_view<'a>(concept: &'a Concept, taxonomy: &'a TaxonomySet) -> ConceptView<'a> {
289 let labels = taxonomy.labels(&concept.name).unwrap_or(&[]);
290 let references = concept
291 .id
292 .as_deref()
293 .and_then(|id| taxonomy.references_for(id))
294 .unwrap_or(&[]);
295 let parent_tuple = concept
296 .id
297 .as_deref()
298 .and_then(|id| taxonomy.find_parent_tuple(id));
299 let tuple_ancestors = concept
300 .id
301 .as_deref()
302 .map(|id| {
303 taxonomy
304 .tuple_ancestor_ids(id)
305 .into_iter()
306 .filter_map(|ancestor_id| taxonomy.find_concept_by_id(&ancestor_id))
307 .collect::<Vec<_>>()
308 })
309 .unwrap_or_default();
310
311 let mut presentation_parents = Vec::new();
312 let mut presentation_children = Vec::new();
313
314 for (role, arcs) in taxonomy.presentations() {
315 for arc in arcs {
316 if arc.to == concept.name {
317 presentation_parents.push(PresentationRelationView {
318 role: role.as_str(),
319 concept_name: &arc.from,
320 concept: taxonomy.find_concept(&arc.from),
321 order: arc.order,
322 preferred_label: arc.preferred_label.as_ref().map(|x| x.as_ref()),
323 });
324 }
325
326 if arc.from == concept.name {
327 presentation_children.push(PresentationRelationView {
328 role: role.as_str(),
329 concept_name: &arc.to,
330 concept: taxonomy.find_concept(&arc.to),
331 order: arc.order,
332 preferred_label: arc.preferred_label.as_ref().map(|x| x.as_ref()),
333 });
334 }
335 }
336 }
337
338 let tuple_content = concept
339 .content_model
340 .as_ref()
341 .map(|particle| project_particle(particle, taxonomy));
342
343 ConceptView {
344 concept,
345 labels,
346 references,
347 parent_tuple,
348 tuple_ancestors,
349 presentation_parents,
350 presentation_children,
351 tuple_content,
352 }
353}
354
355fn project_particle<'a>(
356 particle: &'a Particle,
357 taxonomy: &'a TaxonomySet,
358) -> TupleParticleView<'a> {
359 match particle {
360 Particle::Element { element, occurs } => {
361 let local_name = element.local_name();
362 TupleParticleView::Element {
363 element: TupleElementView {
364 local_name,
365 concept: find_concept_by_local_name(taxonomy, local_name),
366 },
367 occurs,
368 }
369 }
370 Particle::Sequence { children, occurs } => TupleParticleView::Sequence {
371 children: children
372 .iter()
373 .map(|child| project_particle(child, taxonomy))
374 .collect(),
375 occurs,
376 },
377 Particle::Choice { children, occurs } => TupleParticleView::Choice {
378 children: children
379 .iter()
380 .map(|child| project_particle(child, taxonomy))
381 .collect(),
382 occurs,
383 },
384 Particle::Group { group, occurs } => match group {
385 GroupParticle::Ref(qname) => TupleParticleView::GroupRef {
386 name: &qname.local_name,
387 occurs,
388 },
389 GroupParticle::Def(group_def) => TupleParticleView::GroupDef {
390 name: group_def
391 .name
392 .as_ref()
393 .map(|qname| qname.local_name.as_str()),
394 particle: Box::new(project_particle(&group_def.particle, taxonomy)),
395 occurs,
396 },
397 },
398 }
399}
400
401fn find_concept_by_local_name<'a>(
402 taxonomy: &'a TaxonomySet,
403 local_name: &str,
404) -> Option<&'a Concept> {
405 taxonomy
406 .concepts()
407 .into_iter()
408 .find(|concept| concept.name.local_name == local_name)
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414 use crate::{ExpandedName, PresentationArc, taxonomy::TaxonomySet};
415 use rust_decimal::Decimal;
416
417 #[test]
418 fn taxonomy_view_empty_taxonomy() {
419 let taxonomy = TaxonomySet::default();
420 let view = TaxonomyView::build(&taxonomy);
421
422 assert!(view.sections.is_empty());
423 }
424
425 #[test]
426 fn taxonomy_view_missing_role_returns_none() {
427 let taxonomy = TaxonomySet::default();
428 let view = TaxonomyView::build_for_role(&taxonomy, "http://example.com/role");
429
430 assert!(view.is_none());
431 }
432
433 #[test]
434 fn taxonomy_view_section_created_for_existing_role_without_concepts() {
435 let mut taxonomy = TaxonomySet::default();
436 taxonomy.add_presentation_arc(
437 "http://example.com/role".to_string(),
438 PresentationArc {
439 from: ExpandedName::new("http://example.com/ns".into(), "root".into()),
440 to: ExpandedName::new("http://example.com/ns".into(), "child".into()),
441 order: Some(Decimal::new(1, 0)),
442 preferred_label: None,
443 arcrole: "http://www.xbrl.org/2003/arcrole/parent-child".into(),
444 },
445 );
446
447 let view = TaxonomyView::build(&taxonomy);
448
449 assert_eq!(view.sections.len(), 1);
450 assert_eq!(view.sections[0].role, "http://example.com/role");
451 assert!(view.sections[0].nodes.is_empty());
453 }
454
455 #[test]
456 fn concept_view_build_from_name_returns_none_for_missing_concept() {
457 let taxonomy = TaxonomySet::default();
458 let view = ConceptView::build_from_name(
459 &ExpandedName::new("http://example.com/ns".into(), "missing".into()),
460 &taxonomy,
461 );
462
463 assert!(view.is_none());
464 }
465
466 #[test]
467 fn concept_view_build_from_id_returns_none_for_missing_concept() {
468 let taxonomy = TaxonomySet::default();
469 let view = ConceptView::build_from_id("missing", &taxonomy);
470
471 assert!(view.is_none());
472 }
473}