Skip to main content

laminar_connectors/fpml/
references.rs

1//! Document-local reference integrity: every FpML `href` must name an element `id` in the same
2//! document, and that element must have the type the schema's `ecore:reference` annotation
3//! requires (FpML rules `ref-*`). `href` is never fetched or treated as a URL.
4
5use std::collections::{HashMap, HashSet};
6
7use super::document::{Document, Element, Interner, NameId};
8use super::error::{Rejection, Stage};
9use super::schema_model::SchemaModel;
10
11/// How far `href` checking goes beyond resolution.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ReferencePolicy {
14    /// Every `href` resolves to a unique `id` in the document; a reference the projection
15    /// dereferences must also reach the annotated target type.
16    Resolve,
17    /// Additionally, every `href` reaches the annotated target type. Official FpML 5.13 examples
18    /// violate annotations the FpML validation rules do not enforce, so this is opt-in.
19    Schema,
20}
21
22/// Interned reference rules for one closure.
23#[derive(Debug, Default)]
24pub(crate) struct ReferenceRules {
25    /// Referencing element (or `xsi:type`) to the types its target may have.
26    targets: HashMap<NameId, Vec<NameId>>,
27    /// Element name to every type its declarations are or derive from.
28    element_types: HashMap<NameId, HashSet<NameId>>,
29    /// Type to itself and its ancestors, for elements carrying `xsi:type`.
30    type_ancestors: HashMap<NameId, HashSet<NameId>>,
31}
32
33impl ReferenceRules {
34    pub(crate) fn new(model: &SchemaModel, interner: &mut Interner) -> Self {
35        let mut rules = Self::default();
36        for (element, types) in &model.element_types {
37            let name = interner.intern(&element.0, &element.1);
38            let ancestors = rules.element_types.entry(name).or_default();
39            for ty in types {
40                for ancestor in model.ancestors(ty) {
41                    ancestors.insert(interner.intern(&ancestor.0, &ancestor.1));
42                }
43            }
44            if let Some(targets) = model.reference_targets_of(element) {
45                rules.targets.insert(
46                    name,
47                    targets
48                        .iter()
49                        .map(|target| interner.intern(&target.0, &target.1))
50                        .collect(),
51                );
52            }
53        }
54        for ty in model.bases.keys().chain(model.reference_targets.keys()) {
55            let id = interner.intern(&ty.0, &ty.1);
56            let ancestors = model
57                .ancestors(ty)
58                .map(|ancestor| interner.intern(&ancestor.0, &ancestor.1))
59                .collect::<HashSet<_>>();
60            if let Some(target) = model.reference_target_of_type(ty) {
61                rules
62                    .targets
63                    .entry(id)
64                    .or_insert_with(|| vec![interner.intern(&target.0, &target.1)]);
65            }
66            rules.type_ancestors.insert(id, ancestors);
67        }
68        rules
69    }
70
71    /// Check every `href` in `document`.
72    pub(crate) fn check(
73        &self,
74        document: &Document,
75        interner: &Interner,
76        href: NameId,
77        policy: ReferencePolicy,
78    ) -> Result<(), Rejection> {
79        for &source_id in &document.references {
80            let Some(source) = document.element(source_id) else {
81                continue;
82            };
83            let value = document.attribute(source, href).unwrap_or_default().trim();
84            let Some(target) = document
85                .dereference(value)
86                .and_then(|id| document.element(id))
87            else {
88                return Err(Rejection::new(
89                    Stage::References,
90                    format!(
91                        "line {}: '{}' refers to an id that does not exist in the document",
92                        source.line,
93                        interner.local(source.name)
94                    ),
95                ));
96            };
97            if policy == ReferencePolicy::Schema {
98                self.check_target(source, target, interner)?;
99            }
100        }
101        Ok(())
102    }
103
104    /// Reject a reference whose target lacks the annotated type. Unannotated or ambiguous
105    /// reference names are not checked.
106    pub(crate) fn check_target(
107        &self,
108        source: &Element,
109        target: &Element,
110        interner: &Interner,
111    ) -> Result<(), Rejection> {
112        let allowed = source
113            .xsi_type
114            .and_then(|ty| self.targets.get(&ty))
115            .or_else(|| self.targets.get(&source.name));
116        let Some(allowed) = allowed else {
117            return Ok(());
118        };
119        let target_types = target
120            .xsi_type
121            .and_then(|ty| self.type_ancestors.get(&ty))
122            .or_else(|| self.element_types.get(&target.name));
123        if target_types.is_some_and(|types| allowed.iter().any(|ty| types.contains(ty))) {
124            return Ok(());
125        }
126        Err(Rejection::new(
127            Stage::References,
128            format!(
129                "line {}: '{}' refers to a '{}' element, which is not a {}",
130                source.line,
131                interner.local(source.name),
132                interner.local(target.name),
133                allowed
134                    .iter()
135                    .map(|ty| interner.local(*ty))
136                    .collect::<Vec<_>>()
137                    .join(" or ")
138            ),
139        ))
140    }
141}