1use crate::{
2 ConceptId, ExpandedName, RoleUri, XbrlError,
3 taxonomy::{
4 Concept,
5 linkbases::parser::{LabelResource, RawLinkbases, ReferenceResource},
6 },
7 xml::ArcroleUri,
8};
9use indexmap::IndexMap;
10use rust_decimal::Decimal;
11use std::collections::HashMap;
12
13#[derive(Debug, Clone, PartialEq)]
15pub struct Reference {
16 pub role: String,
18}
19
20#[derive(Debug, Clone, PartialEq)]
22pub struct ReferencePart {
23 pub name: String,
25 pub value: String,
27}
28
29#[derive(Debug, Clone, PartialEq)]
31pub struct Label {
32 pub role: String,
34 pub lang: String,
36 pub text: String,
38}
39
40#[derive(Debug, Clone, PartialEq)]
42pub struct PresentationArc {
43 pub from: ExpandedName,
45 pub to: ExpandedName,
47 pub order: Option<Decimal>,
49 pub preferred_label: Option<RoleUri>,
51 pub arcrole: ArcroleUri,
53}
54
55#[derive(Debug, Clone, PartialEq)]
57pub struct CalculationArc {
58 pub from: ExpandedName,
60 pub to: ExpandedName,
62 pub order: Option<Decimal>,
64 pub weight: Decimal,
66 pub arcrole: ArcroleUri,
68}
69
70#[derive(Debug, Clone, PartialEq)]
72pub struct DefinitionArc {
73 pub from: ExpandedName,
75 pub to: ExpandedName,
77 pub order: Option<Decimal>,
79 pub arcrole: ArcroleUri,
81}
82
83#[derive(Debug, Default)]
99pub struct Linkbases {
100 pub presentations: IndexMap<RoleUri, Vec<PresentationArc>>,
103 pub calculations: HashMap<RoleUri, Vec<CalculationArc>>,
105 pub definitions: HashMap<RoleUri, Vec<DefinitionArc>>,
107 pub labels: HashMap<ExpandedName, Vec<Label>>,
110 pub references: HashMap<ConceptId, Vec<Reference>>,
113}
114
115pub fn resolve_linkbases(
118 linkbases: RawLinkbases,
119 concepts_by_id: &HashMap<ConceptId, &Concept>,
120) -> Result<Linkbases, XbrlError> {
121 let mut labels: HashMap<ExpandedName, Vec<Label>> = HashMap::new();
122 let mut presentations: IndexMap<RoleUri, Vec<PresentationArc>> = IndexMap::new();
123 let mut calculations: HashMap<RoleUri, Vec<CalculationArc>> = HashMap::new();
124 let mut definitions: HashMap<RoleUri, Vec<DefinitionArc>> = HashMap::new();
125 let mut references: HashMap<ConceptId, Vec<Reference>> = HashMap::new();
126
127 for link in linkbases.presentation_links {
128 let locator_map: HashMap<&str, &str> = link
130 .locators
131 .iter()
132 .filter_map(|locator| {
133 href_fragment(&locator.href).map(|fragment| (locator.label.as_str(), fragment))
134 })
135 .collect();
136 let arcs: Vec<PresentationArc> = link
137 .arcs
138 .into_iter()
139 .filter_map(|arc| {
140 let from_fragment = locator_map.get(arc.from.as_str())?;
141 let from_concept = concepts_by_id.get(&ConceptId::from(*from_fragment))?;
142 let to_fragment = locator_map.get(arc.to.as_str())?;
143 let to_concept = concepts_by_id.get(&ConceptId::from(*to_fragment))?;
144
145 Some(PresentationArc {
146 from: from_concept.name.clone(),
147 to: to_concept.name.clone(),
148 order: arc.order,
149 preferred_label: arc.preferred_label.clone(),
150 arcrole: arc.arcrole.clone(),
151 })
152 })
153 .collect();
154
155 if !arcs.is_empty() {
156 presentations
157 .entry(link.role.into())
158 .or_default()
159 .extend(arcs);
160 }
161 }
162
163 for link in linkbases.calculation_links {
164 let locator_map: HashMap<&str, &str> = link
166 .locators
167 .iter()
168 .filter_map(|locator| {
169 href_fragment(&locator.href).map(|fragment| (locator.label.as_str(), fragment))
170 })
171 .collect();
172 let arcs: Vec<CalculationArc> = link
173 .arcs
174 .into_iter()
175 .filter_map(|arc| {
176 let from_fragment = locator_map.get(arc.from.as_str())?;
177 let from_concept = concepts_by_id.get(&ConceptId::from(*from_fragment))?;
178 let to_fragment = locator_map.get(arc.to.as_str())?;
179 let to_concept = concepts_by_id.get(&ConceptId::from(*to_fragment))?;
180
181 Some(CalculationArc {
182 from: from_concept.name.clone(),
183 to: to_concept.name.clone(),
184 order: arc.order,
185 weight: arc.weight,
186 arcrole: arc.arcrole.clone(),
187 })
188 })
189 .collect();
190
191 if !arcs.is_empty() {
192 calculations
193 .entry(link.role.into())
194 .or_default()
195 .extend(arcs);
196 }
197 }
198
199 for link in linkbases.definition_links {
200 let locator_map: HashMap<&str, &str> = link
202 .locators
203 .iter()
204 .filter_map(|locator| {
205 href_fragment(&locator.href).map(|fragment| (locator.label.as_str(), fragment))
206 })
207 .collect();
208 let arcs: Vec<DefinitionArc> = link
209 .arcs
210 .into_iter()
211 .filter_map(|arc| {
212 let from_fragment = locator_map.get(arc.from.as_str())?;
213 let from_concept = concepts_by_id.get(&ConceptId::from(*from_fragment))?;
214 let to_fragment = locator_map.get(arc.to.as_str())?;
215 let to_concept = concepts_by_id.get(&ConceptId::from(*to_fragment))?;
216
217 Some(DefinitionArc {
218 from: from_concept.name.clone(),
219 to: to_concept.name.clone(),
220 order: arc.order,
221 arcrole: arc.arcrole.clone(),
222 })
223 })
224 .collect();
225
226 if !arcs.is_empty() {
227 definitions
228 .entry(link.role.into())
229 .or_default()
230 .extend(arcs);
231 }
232 }
233
234 for link in linkbases.label_links {
235 let locator_map: HashMap<&str, &str> = link
236 .locators
237 .iter()
238 .filter_map(|locator| {
239 href_fragment(&locator.href).map(|fragment| (locator.label.as_str(), fragment))
240 })
241 .collect();
242 let resource_map: HashMap<&str, &LabelResource> = link
243 .labels
244 .iter()
245 .map(|resource| (resource.label.as_str(), resource))
246 .collect();
247
248 for arc in &link.arcs {
249 if let (Some(&concept_id), Some(&resource)) = (
250 locator_map.get(arc.from.as_str()),
251 resource_map.get(arc.to.as_str()),
252 ) {
253 if let Some(concept) = concepts_by_id.get(&ConceptId::from(concept_id)) {
254 labels.entry(concept.name.clone()).or_default().push(Label {
255 role: resource.role.clone().unwrap_or_default(),
256 lang: resource.lang.clone(),
257 text: resource.text.clone(),
258 });
259 } else {
260 return Err(XbrlError::InvalidLinkbaseResolution {
261 reason: format!(
262 "Label linkbase refers to unknown concept ID '{}'",
263 concept_id
264 ),
265 });
266 }
267 }
268 }
269 }
270
271 for link in linkbases.reference_links {
272 let locator_map: HashMap<&str, &str> = link
274 .locators
275 .iter()
276 .filter_map(|locator| {
277 href_fragment(&locator.href).map(|fragment| (locator.label.as_str(), fragment))
278 })
279 .collect();
280 let resource_map: HashMap<&str, &ReferenceResource> = link
281 .references
282 .iter()
283 .map(|resource| (resource.label.as_str(), resource))
284 .collect();
285
286 for arc in &link.arcs {
287 if let (Some(&concept_id), Some(&resource)) = (
288 locator_map.get(arc.from.as_str()),
289 resource_map.get(arc.to.as_str()),
290 ) {
291 references
292 .entry(concept_id.into())
293 .or_default()
294 .push(Reference {
295 role: resource.role.clone().unwrap_or_default(),
296 });
297 }
298 }
299 }
300
301 Ok(Linkbases {
302 presentations,
303 calculations,
304 definitions,
305 labels,
306 references,
307 })
308}
309
310fn href_fragment(href: &str) -> Option<&str> {
312 href.split_once('#').map(|(_, frag)| frag)
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use crate::taxonomy::{
319 linkbases::parser::{
320 CalculationLink, DefinitionLink, LabelLink, Locator, PresentationLink,
321 RawCalculationArc, RawDefinitionArc, RawLabelArc, RawPresentationArc, RawReferenceArc,
322 ReferenceLink,
323 },
324 schema::{BaseSubstitutionGroup, PeriodType, SubstitutionGroup, XbrlType},
325 };
326
327 fn create_concepts() -> Vec<Concept> {
328 vec![
329 Concept {
330 name: ExpandedName::new("http://example.com".into(), "concept1".to_string()),
331 id: Some("concept1".to_string()),
332 data_type: XbrlType::Monetary,
333 substitution_group: SubstitutionGroup {
334 base: BaseSubstitutionGroup::Item,
335 original: ExpandedName::new(
336 "http://www.xbrl.org/2003/instance".into(),
337 "item".to_string(),
338 ),
339 },
340 period_type: Some(PeriodType::Instant),
341 balance: None,
342 nillable: false,
343 is_abstract: false,
344 tuple_children: Vec::new(),
345 compositor: None,
346 },
347 Concept {
348 name: ExpandedName::new("http://example.com".into(), "concept2".to_string()),
349 id: Some("concept2".to_string()),
350 data_type: XbrlType::Monetary,
351 substitution_group: SubstitutionGroup {
352 base: BaseSubstitutionGroup::Item,
353 original: ExpandedName::new(
354 "http://www.xbrl.org/2003/instance".into(),
355 "item".to_string(),
356 ),
357 },
358 period_type: Some(PeriodType::Instant),
359 balance: None,
360 nillable: false,
361 is_abstract: false,
362 tuple_children: Vec::new(),
363 compositor: None,
364 },
365 ]
366 }
367
368 #[test]
369 fn test_resolve_linkbases() {
370 let concepts = create_concepts();
371 let concepts_by_id = concepts
372 .iter()
373 .map(|concept| (ConceptId::from(concept.id.clone().unwrap()), concept))
374 .collect::<HashMap<_, _>>();
375 let raw_presentation = RawLinkbases {
376 presentation_links: vec![PresentationLink {
377 role: "http://example.com/role/presentation".into(),
378 locators: vec![
379 Locator {
380 label: "loc1".into(),
381 href: "schema.xsd#concept1".into(),
382 },
383 Locator {
384 label: "loc2".into(),
385 href: "schema.xsd#concept2".into(),
386 },
387 ],
388 arcs: vec![RawPresentationArc {
389 from: "loc1".into(),
390 to: "loc2".into(),
391 order: Some(Decimal::new(1, 0)),
392 preferred_label: None,
393 arcrole: "http://www.xbrl.org/2003/arcrole/parent-child".into(),
394 }],
395 }],
396 calculation_links: vec![CalculationLink {
397 role: "http://example.com/role/calculation".into(),
398 locators: vec![
399 Locator {
400 label: "loc1".into(),
401 href: "schema.xsd#concept1".into(),
402 },
403 Locator {
404 label: "loc2".into(),
405 href: "schema.xsd#concept2".into(),
406 },
407 ],
408 arcs: vec![RawCalculationArc {
409 from: "loc1".into(),
410 to: "loc2".into(),
411 order: Some(Decimal::new(1, 0)),
412 weight: Decimal::new(1, 0),
413 arcrole: "http://www.xbrl.org/2003/arcrole/summation-item".into(),
414 }],
415 }],
416 definition_links: vec![DefinitionLink {
417 role: "http://example.com/role/definition".into(),
418 locators: vec![
419 Locator {
420 label: "loc1".into(),
421 href: "schema.xsd#concept1".into(),
422 },
423 Locator {
424 label: "loc2".into(),
425 href: "schema.xsd#concept2".into(),
426 },
427 ],
428 arcs: vec![RawDefinitionArc {
429 from: "loc1".into(),
430 to: "loc2".into(),
431 order: Some(Decimal::new(1, 0)),
432 arcrole: "http://www.xbrl.org/2003/arcrole/parent-child".into(),
433 }],
434 }],
435 label_links: vec![LabelLink {
436 role: "http://example.com/role/label".into(),
437 locators: vec![Locator {
438 label: "loc1".into(),
439 href: "schema.xsd#concept1".into(),
440 }],
441 labels: vec![LabelResource {
442 label: "lab1".into(),
443 role: Some("http://www.xbrl.org/2003/role/label".into()),
444 lang: "en".into(),
445 text: "Concept 1 Label".into(),
446 }],
447 arcs: vec![RawLabelArc {
448 from: "loc1".into(),
449 to: "lab1".into(),
450 }],
451 }],
452 reference_links: vec![ReferenceLink {
453 role: "http://example.com/role/reference".into(),
454 locators: vec![Locator {
455 label: "loc1".into(),
456 href: "schema.xsd#concept1".into(),
457 }],
458 references: vec![ReferenceResource {
459 label: "ref1".into(),
460 role: Some("http://www.xbrl.org/2003/role/reference".into()),
461 }],
462 arcs: vec![RawReferenceArc {
463 from: "loc1".into(),
464 to: "ref1".into(),
465 }],
466 }],
467 };
468 let linkbases = resolve_linkbases(raw_presentation, &concepts_by_id).unwrap();
469 assert_eq!(linkbases.presentations.len(), 1);
470 assert_eq!(linkbases.calculations.len(), 1);
471 assert_eq!(linkbases.definitions.len(), 1);
472 assert_eq!(linkbases.labels.len(), 1);
473 assert_eq!(linkbases.references.len(), 1);
474
475 let presentation_arc = &linkbases.presentations["http://example.com/role/presentation"][0];
476 assert_eq!(
477 presentation_arc,
478 &PresentationArc {
479 from: ExpandedName::new("http://example.com".into(), "concept1".to_string()),
480 to: ExpandedName::new("http://example.com".into(), "concept2".to_string()),
481 order: Some(Decimal::new(1, 0)),
482 preferred_label: None,
483 arcrole: "http://www.xbrl.org/2003/arcrole/parent-child".into(),
484 }
485 );
486
487 let calculation_arc = &linkbases.calculations["http://example.com/role/calculation"][0];
488 assert_eq!(
489 calculation_arc,
490 &CalculationArc {
491 from: ExpandedName::new("http://example.com".into(), "concept1".to_string()),
492 to: ExpandedName::new("http://example.com".into(), "concept2".to_string()),
493 order: Some(Decimal::new(1, 0)),
494 weight: Decimal::new(1, 0),
495 arcrole: "http://www.xbrl.org/2003/arcrole/summation-item".into(),
496 }
497 );
498
499 let definition_arc = &linkbases.definitions["http://example.com/role/definition"][0];
500 assert_eq!(
501 definition_arc,
502 &DefinitionArc {
503 from: ExpandedName::new("http://example.com".into(), "concept1".to_string()),
504 to: ExpandedName::new("http://example.com".into(), "concept2".to_string()),
505 order: Some(Decimal::new(1, 0)),
506 arcrole: "http://www.xbrl.org/2003/arcrole/parent-child".into(),
507 }
508 );
509
510 let label = &linkbases.labels
511 [&ExpandedName::new("http://example.com".into(), "concept1".to_string())][0];
512 assert_eq!(
513 label,
514 &Label {
515 role: "http://www.xbrl.org/2003/role/label".into(),
516 lang: "en".into(),
517 text: "Concept 1 Label".into(),
518 }
519 );
520
521 let reference = &linkbases.references[&ConceptId::from("concept1".to_string())][0];
522 assert_eq!(
523 reference,
524 &Reference {
525 role: "http://www.xbrl.org/2003/role/reference".into(),
526 }
527 );
528 }
529}