use super::{
Concept, GroupParticle, Label, Occurrence, Particle, PresentationArc, Reference, TaxonomySet,
};
use crate::ExpandedName;
use rust_decimal::Decimal;
use std::{
cmp::Ordering,
collections::{HashMap, HashSet},
};
#[derive(Debug)]
pub struct TaxonomyView<'a> {
pub sections: Vec<TaxonomySectionView<'a>>,
}
impl<'a> TaxonomyView<'a> {
pub fn build(taxonomy: &'a TaxonomySet) -> Self {
build_taxonomy_view(taxonomy)
}
pub fn build_for_role(taxonomy: &'a TaxonomySet, role: &'a str) -> Option<Self> {
taxonomy.presentation_arcs(role).map(|arcs| TaxonomyView {
sections: vec![build_section(role, arcs, taxonomy)],
})
}
}
#[derive(Debug)]
pub struct TaxonomySectionView<'a> {
pub role: &'a str,
pub nodes: Vec<TaxonomyTreeNode<'a>>,
}
#[derive(Debug)]
pub struct TaxonomyTreeNode<'a> {
pub concept: &'a Concept,
pub labels: &'a [Label],
pub depth: usize,
pub children: Vec<TaxonomyTreeNode<'a>>,
}
#[derive(Debug)]
pub struct ConceptView<'a> {
pub concept: &'a Concept,
pub labels: &'a [Label],
pub references: &'a [Reference],
pub parent_tuple: Option<&'a Concept>,
pub tuple_ancestors: Vec<&'a Concept>,
pub presentation_parents: Vec<PresentationRelationView<'a>>,
pub presentation_children: Vec<PresentationRelationView<'a>>,
pub tuple_content: Option<TupleParticleView<'a>>,
}
impl<'a> ConceptView<'a> {
pub fn build(concept: &'a Concept, taxonomy: &'a TaxonomySet) -> Self {
build_concept_view(concept, taxonomy)
}
pub fn build_from_name(concept_name: &ExpandedName, taxonomy: &'a TaxonomySet) -> Option<Self> {
taxonomy
.find_concept(concept_name)
.map(|concept| build_concept_view(concept, taxonomy))
}
pub fn build_from_id(concept_id: &str, taxonomy: &'a TaxonomySet) -> Option<Self> {
taxonomy
.find_concept_by_id(concept_id)
.map(|concept| build_concept_view(concept, taxonomy))
}
}
#[derive(Debug)]
pub struct PresentationRelationView<'a> {
pub role: &'a str,
pub concept_name: &'a ExpandedName,
pub concept: Option<&'a Concept>,
pub order: Option<Decimal>,
pub preferred_label: Option<&'a str>,
}
#[derive(Debug)]
pub enum TupleParticleView<'a> {
Element {
element: TupleElementView<'a>,
occurs: &'a Occurrence,
},
Sequence {
children: Vec<TupleParticleView<'a>>,
occurs: &'a Occurrence,
},
Choice {
children: Vec<TupleParticleView<'a>>,
occurs: &'a Occurrence,
},
GroupRef {
name: &'a str,
occurs: &'a Occurrence,
},
GroupDef {
name: Option<&'a str>,
particle: Box<TupleParticleView<'a>>,
occurs: &'a Occurrence,
},
}
#[derive(Debug)]
pub struct TupleElementView<'a> {
pub local_name: &'a str,
pub concept: Option<&'a Concept>,
}
fn build_taxonomy_view<'a>(taxonomy: &'a TaxonomySet) -> TaxonomyView<'a> {
let sections = taxonomy
.presentations()
.iter()
.map(|(role, arcs)| build_section(role.as_str(), arcs, taxonomy))
.collect();
TaxonomyView { sections }
}
fn build_section<'a>(
role: &'a str,
arcs: &'a [PresentationArc],
taxonomy: &'a TaxonomySet,
) -> TaxonomySectionView<'a> {
let concept_index = taxonomy
.concepts()
.map(|concept| (&concept.name, concept))
.collect::<HashMap<_, _>>();
let mut arc_index: HashMap<&'a ExpandedName, Vec<&'a PresentationArc>> = HashMap::new();
for arc in arcs {
arc_index.entry(&arc.from).or_default().push(arc);
}
for children in arc_index.values_mut() {
children.sort_by(|a, b| match (a.order, b.order) {
(Some(x), Some(y)) => x.cmp(&y),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => Ordering::Equal,
});
}
let roots = find_roots(arcs, &arc_index);
let mut visited: HashSet<&'a ExpandedName> = HashSet::new();
let mut nodes = Vec::with_capacity(roots.len());
for root_id in roots {
if let Some(node) = build_node(
&concept_index,
&arc_index,
root_id,
0,
taxonomy,
&mut visited,
) {
nodes.push(node);
}
}
TaxonomySectionView { role, nodes }
}
fn find_roots<'a>(
arcs: &'a [PresentationArc],
arc_index: &HashMap<&'a ExpandedName, Vec<&'a PresentationArc>>,
) -> Vec<&'a ExpandedName> {
let to_set: HashSet<&ExpandedName> = arcs.iter().map(|arc| &arc.to).collect();
let mut roots: Vec<&'a ExpandedName> = arc_index
.keys()
.copied()
.filter(|from| !to_set.contains(*from))
.collect();
roots.sort_by(|a, b| {
let min_order = |id: &&ExpandedName| {
arc_index
.get(*id)
.and_then(|children| children.iter().filter_map(|arc| arc.order).min())
};
match (min_order(a), min_order(b)) {
(Some(x), Some(y)) => x.cmp(&y),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => Ordering::Equal,
}
});
roots
}
fn build_node<'a>(
concept_index: &HashMap<&'a ExpandedName, &'a Concept>,
arc_index: &HashMap<&'a ExpandedName, Vec<&'a PresentationArc>>,
concept_id: &'a ExpandedName,
depth: usize,
taxonomy: &'a TaxonomySet,
visited: &mut HashSet<&'a ExpandedName>,
) -> Option<TaxonomyTreeNode<'a>> {
if !visited.insert(concept_id) {
return None;
}
let concept = concept_index.get(concept_id).copied()?;
let labels = taxonomy.labels(concept_id).unwrap_or(&[]);
let child_arcs = arc_index.get(concept_id).map(Vec::as_slice).unwrap_or(&[]);
let mut children = Vec::with_capacity(child_arcs.len());
for arc in child_arcs {
if let Some(child_node) = build_node(
concept_index,
arc_index,
&arc.to,
depth + 1,
taxonomy,
visited,
) {
children.push(child_node);
}
}
visited.remove(concept_id);
Some(TaxonomyTreeNode {
concept,
labels,
depth,
children,
})
}
fn build_concept_view<'a>(concept: &'a Concept, taxonomy: &'a TaxonomySet) -> ConceptView<'a> {
let labels = taxonomy.labels(&concept.name).unwrap_or(&[]);
let references = concept
.id
.as_deref()
.and_then(|id| taxonomy.references_for(id))
.unwrap_or(&[]);
let parent_tuple = concept
.id
.as_deref()
.and_then(|id| taxonomy.find_parent_tuple(id));
let tuple_ancestors = concept
.id
.as_deref()
.map(|id| {
taxonomy
.tuple_ancestor_ids(id)
.into_iter()
.filter_map(|ancestor_id| taxonomy.find_concept_by_id(&ancestor_id))
.collect::<Vec<_>>()
})
.unwrap_or_default();
let mut presentation_parents = Vec::new();
let mut presentation_children = Vec::new();
for (role, arcs) in taxonomy.presentations() {
for arc in arcs {
if arc.to == concept.name {
presentation_parents.push(PresentationRelationView {
role: role.as_str(),
concept_name: &arc.from,
concept: taxonomy.find_concept(&arc.from),
order: arc.order,
preferred_label: arc.preferred_label.as_ref().map(|x| x.as_ref()),
});
}
if arc.from == concept.name {
presentation_children.push(PresentationRelationView {
role: role.as_str(),
concept_name: &arc.to,
concept: taxonomy.find_concept(&arc.to),
order: arc.order,
preferred_label: arc.preferred_label.as_ref().map(|x| x.as_ref()),
});
}
}
}
let tuple_content = concept
.content_model
.as_ref()
.map(|particle| project_particle(particle, taxonomy));
ConceptView {
concept,
labels,
references,
parent_tuple,
tuple_ancestors,
presentation_parents,
presentation_children,
tuple_content,
}
}
fn project_particle<'a>(
particle: &'a Particle,
taxonomy: &'a TaxonomySet,
) -> TupleParticleView<'a> {
match particle {
Particle::Element { element, occurs } => {
let local_name = element.local_name();
TupleParticleView::Element {
element: TupleElementView {
local_name,
concept: find_concept_by_local_name(taxonomy, local_name),
},
occurs,
}
}
Particle::Sequence { children, occurs } => TupleParticleView::Sequence {
children: children
.iter()
.map(|child| project_particle(child, taxonomy))
.collect(),
occurs,
},
Particle::Choice { children, occurs } => TupleParticleView::Choice {
children: children
.iter()
.map(|child| project_particle(child, taxonomy))
.collect(),
occurs,
},
Particle::Group { group, occurs } => match group {
GroupParticle::Ref(qname) => TupleParticleView::GroupRef {
name: &qname.local_name,
occurs,
},
GroupParticle::Def(group_def) => TupleParticleView::GroupDef {
name: group_def
.name
.as_ref()
.map(|qname| qname.local_name.as_str()),
particle: Box::new(project_particle(&group_def.particle, taxonomy)),
occurs,
},
},
}
}
fn find_concept_by_local_name<'a>(
taxonomy: &'a TaxonomySet,
local_name: &str,
) -> Option<&'a Concept> {
taxonomy
.concepts()
.into_iter()
.find(|concept| concept.name.local_name == local_name)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ExpandedName, PresentationArc, taxonomy::TaxonomySet};
use rust_decimal::Decimal;
#[test]
fn taxonomy_view_empty_taxonomy() {
let taxonomy = TaxonomySet::default();
let view = TaxonomyView::build(&taxonomy);
assert!(view.sections.is_empty());
}
#[test]
fn taxonomy_view_missing_role_returns_none() {
let taxonomy = TaxonomySet::default();
let view = TaxonomyView::build_for_role(&taxonomy, "http://example.com/role");
assert!(view.is_none());
}
#[test]
fn taxonomy_view_section_created_for_existing_role_without_concepts() {
let mut taxonomy = TaxonomySet::default();
taxonomy.add_presentation_arc(
"http://example.com/role".to_string(),
PresentationArc {
from: ExpandedName::new("http://example.com/ns".into(), "root".into()),
to: ExpandedName::new("http://example.com/ns".into(), "child".into()),
order: Some(Decimal::new(1, 0)),
preferred_label: None,
arcrole: "http://www.xbrl.org/2003/arcrole/parent-child".into(),
},
);
let view = TaxonomyView::build(&taxonomy);
assert_eq!(view.sections.len(), 1);
assert_eq!(view.sections[0].role, "http://example.com/role");
assert!(view.sections[0].nodes.is_empty());
}
#[test]
fn concept_view_build_from_name_returns_none_for_missing_concept() {
let taxonomy = TaxonomySet::default();
let view = ConceptView::build_from_name(
&ExpandedName::new("http://example.com/ns".into(), "missing".into()),
&taxonomy,
);
assert!(view.is_none());
}
#[test]
fn concept_view_build_from_id_returns_none_for_missing_concept() {
let taxonomy = TaxonomySet::default();
let view = ConceptView::build_from_id("missing", &taxonomy);
assert!(view.is_none());
}
}