Skip to main content

xsd_schema/validation/
assertions.rs

1//! XSD 1.1 complex-type assertion evaluation.
2//!
3//! Complex types can carry `xs:assert` elements whose XPath 2.0 expressions
4//! are evaluated against the element subtree. This module provides:
5//!
6//! - [`AssertionBufferFrame`] — per-element bookkeeping for assertion buffering
7//! - [`has_inherited_assertions`] — cheap hot-path check for any assertions
8//! - [`collect_inherited_assertions`] — full base-first collection with owner keys
9//! - [`resolve_ct_assertion_default_ns`] — xpathDefaultNamespace cascade
10//! - [`evaluate_complex_type_assertions`] — core XPath evaluation
11
12use crate::document::buffer::BufferDocument;
13use crate::document::navigator::BufferDocNavigator;
14use crate::ids::{ComplexTypeKey, NameId, TypeKey};
15use crate::navigator::{DomNavigator, TypedValue};
16use crate::parser::frames::{AssertResult, ComplexContentResult};
17use crate::parser::location::SourceLocation;
18use crate::schema::SchemaSet;
19use crate::validation::errors::{self, ValidationError};
20use crate::validation::simple::validate_simple_type;
21use crate::xpath::api::XPathExpr;
22use crate::xpath::functions::{effective_boolean_value, XPathValue};
23use crate::xpath::XPathContext;
24
25use crate::arenas::SchemaArenas;
26
27// ---------------------------------------------------------------------------
28// AssertionBufferFrame
29// ---------------------------------------------------------------------------
30
31/// Per-element assertion buffer frame.
32///
33/// Created when a complex type with assertions is encountered during streaming
34/// validation. Tracks the node reference in the fragment document and the
35/// owning complex type, so assertions can be evaluated at element close.
36pub(crate) struct AssertionBufferFrame {
37    /// Node ref of this element in the fragment document.
38    pub element_ref: u32,
39    /// ComplexType key whose assertions triggered this frame.
40    pub complex_type_key: ComplexTypeKey,
41    /// Element path at the time this frame's element closed (for error reporting).
42    /// Populated when the frame is popped at its own end-element (before deferral).
43    pub element_path: String,
44    /// Source location at the time this frame's element closed.
45    pub location: Option<SourceLocation>,
46}
47
48// ---------------------------------------------------------------------------
49// has_inherited_assertions — cheap hot-path check
50// ---------------------------------------------------------------------------
51
52/// Returns `true` if the complex type (or any base in its derivation chain)
53/// has non-empty `assertions`. No allocation. Used in
54/// `validate_element_by_id` to decide whether to start assertion buffering.
55pub(crate) fn has_inherited_assertions(ct_key: ComplexTypeKey, arenas: &SchemaArenas) -> bool {
56    let ct = &arenas.complex_types[ct_key];
57    if !ct.assertions.is_empty() {
58        return true;
59    }
60    // Walk the derivation chain
61    let mut current = ct.resolved_base_type;
62    while let Some(TypeKey::Complex(base_key)) = current {
63        let base = &arenas.complex_types[base_key];
64        if !base.assertions.is_empty() {
65            return true;
66        }
67        current = base.resolved_base_type;
68    }
69    false
70}
71
72// ---------------------------------------------------------------------------
73// collect_inherited_assertions — full collection
74// ---------------------------------------------------------------------------
75
76/// Collects all assertions from the complex type and its base types,
77/// ordered base-first. Each assertion is paired with its **defining** type's
78/// key — essential for the xpathDefaultNamespace cascade, which must use the
79/// type-level default from the type that declared the assertion.
80pub(crate) fn collect_inherited_assertions(
81    ct_key: ComplexTypeKey,
82    arenas: &SchemaArenas,
83) -> Vec<(&AssertResult, ComplexTypeKey)> {
84    // Collect chain of complex type keys from derived to base
85    let mut chain = vec![ct_key];
86    let mut current = arenas.complex_types[ct_key].resolved_base_type;
87    while let Some(TypeKey::Complex(base_key)) = current {
88        chain.push(base_key);
89        current = arenas.complex_types[base_key].resolved_base_type;
90    }
91
92    // Reverse for base-first order, then collect assertions
93    let mut result = Vec::new();
94    for &key in chain.iter().rev() {
95        let ct = &arenas.complex_types[key];
96        for assertion in &ct.assertions {
97            result.push((assertion, key));
98        }
99    }
100    result
101}
102
103// ---------------------------------------------------------------------------
104// resolve_ct_assertion_default_ns — xpathDefaultNamespace cascade
105// ---------------------------------------------------------------------------
106
107/// Three-level cascade: **assertion-level > owner-type-level > schema-document-level**.
108///
109/// Takes the **owner** `ComplexTypeKey` (from `collect_inherited_assertions`),
110/// not the derived type, so inherited assertions get the correct type-level default.
111fn resolve_ct_assertion_default_ns(
112    assertion: &AssertResult,
113    owner_ct_key: ComplexTypeKey,
114    schema_set: &SchemaSet,
115) -> Option<NameId> {
116    let ct = &schema_set.arenas.complex_types[owner_ct_key];
117
118    // Look up the schema document that defines the owning type
119    let doc = ct
120        .source
121        .as_ref()
122        .and_then(|s| schema_set.documents.get(s.doc_id as usize));
123
124    // Cascade: assertion-level > type-level > schema-document-level
125    let effective = if let Some(raw) = &assertion.xpath_default_namespace {
126        Some(raw.clone())
127    } else if let Some(raw) = &ct.xpath_default_namespace {
128        Some(raw.clone())
129    } else {
130        doc.and_then(|d| d.xpath_default_namespace)
131            .map(|id| schema_set.name_table.resolve(id))
132    };
133
134    match effective.as_deref() {
135        Some("##defaultNamespace") => assertion.ns_snapshot.default_ns,
136        Some("##targetNamespace") => doc.and_then(|d| d.target_namespace),
137        Some("##local") | None => None,
138        Some(uri) => Some(schema_set.name_table.add(uri)),
139    }
140}
141
142// ---------------------------------------------------------------------------
143// compute_dollar_value — XSD 1.1 §3.13.4.1 clause 2.3 binding
144// ---------------------------------------------------------------------------
145
146/// Compute the value of `$value` for an assertion.
147///
148/// Per §3.13.4.1 clause 2.3, `$value` is bound from **E's governing
149/// type definition** (the most-derived type for the element), not from
150/// each inherited assertion's owner. So this is computed once per
151/// element and reused across all assertions in the inheritance chain:
152/// - Governing type's content variety **simple**, element not nilled,
153///   simple-type validation succeeds → the typed value.
154/// - Otherwise → empty sequence (clause 2.3.2).
155///
156/// The partial-PSVI `[validity]` is unavailable here, so any
157/// simple-type-validation failure falls into the empty-sequence branch.
158fn compute_dollar_value<'doc>(
159    doc: &'doc BufferDocument<'doc>,
160    element_ref: u32,
161    governing_ct_key: ComplexTypeKey,
162    schema_set: &SchemaSet,
163) -> XPathValue<BufferDocNavigator<'doc>> {
164    use crate::types::value::{XmlValue, XmlValueKind};
165    use crate::xpath::iterator::XmlItem;
166
167    let ct = &schema_set.arenas.complex_types[governing_ct_key];
168    if !matches!(ct.content, ComplexContentResult::Simple(_)) {
169        return XPathValue::empty();
170    }
171
172    let nav = BufferDocNavigator::new(doc, element_ref);
173    if matches!(nav.typed_value(), TypedValue::Nilled) {
174        return XPathValue::empty();
175    }
176
177    match validate_simple_type(&nav.value(), TypeKey::Complex(governing_ct_key), schema_set) {
178        Ok(result) => {
179            // §3.13.4.1 clause 2.3.1.4: when the governing simple-content type's
180            // {variety} = list, `$value` is a sequence of atomic values, one per
181            // list item. The simple-type validator stores list items in
182            // `XmlValueKind::List`; unwrap to a sequence so XPath sees a
183            // multi-item input.
184            if let XmlValueKind::List { item_type, items } = &result.typed_value.value {
185                let item_type_code = *item_type;
186                let xpath_items: Vec<XmlItem<BufferDocNavigator<'doc>>> = items
187                    .iter()
188                    .cloned()
189                    .map(|atom| {
190                        XmlItem::Atomic(XmlValue::new(item_type_code, XmlValueKind::Atomic(atom)))
191                    })
192                    .collect();
193                return XPathValue::from_sequence(xpath_items);
194            }
195            XPathValue::from_atomic(result.typed_value)
196        }
197        Err(_) => XPathValue::empty(),
198    }
199}
200
201// ---------------------------------------------------------------------------
202// evaluate_complex_type_assertions — core evaluation
203// ---------------------------------------------------------------------------
204
205/// Evaluate all assertions (own + inherited) for a complex type against
206/// the element subtree in a `BufferDocument`.
207///
208/// Returns a `Vec` of all `cvc-assertion` errors (does not stop at first failure).
209pub(crate) fn evaluate_complex_type_assertions(
210    doc: &BufferDocument<'_>,
211    element_ref: u32,
212    ct_key: ComplexTypeKey,
213    schema_set: &SchemaSet,
214) -> Vec<ValidationError> {
215    let assertions = collect_inherited_assertions(ct_key, &schema_set.arenas);
216    let mut errors = Vec::new();
217
218    // §3.13.4.1 clause 2.3 ties `$value` to E's governing type
219    // (the parameter `ct_key`), so it is identical across all
220    // inherited assertions. Compute once and clone per evaluation.
221    let dollar_value = compute_dollar_value(doc, element_ref, ct_key, schema_set);
222
223    for (assertion, owner_key) in assertions {
224        if assertion.test.is_empty() {
225            continue;
226        }
227
228        // Build XPath static context with schema-time namespace snapshot
229        let ctx = XPathContext::new(&schema_set.name_table)
230            .with_namespaces(assertion.ns_snapshot.clone())
231            .with_schema_set(schema_set);
232
233        // Apply xpathDefaultNamespace cascade
234        let ctx = if let Some(default_ns) =
235            resolve_ct_assertion_default_ns(assertion, owner_key, schema_set)
236        {
237            ctx.with_default_element_ns(default_ns)
238        } else {
239            ctx
240        };
241
242        // §3.13.4.1 clause 2.2: `$value` is in scope for every assertion.
243        // Declared unconditionally so XPath that references it compiles.
244        let expr = match XPathExpr::compile_with_vars(&assertion.test, &ctx, &["value"]) {
245            Ok(e) => e,
246            Err(e) => {
247                errors.push(errors::error(
248                    "cvc-assertion",
249                    format!(
250                        "Failed to compile assertion test '{}': {}",
251                        assertion.test, e
252                    ),
253                    None,
254                ));
255                continue;
256            }
257        };
258
259        let nav = BufferDocNavigator::new_assertion(doc, element_ref);
260        let value_for_eval = dollar_value.clone();
261
262        let result = match expr
263            .evaluator(&ctx)
264            .run_with_node_and_setup(Some(nav), |eval| {
265                eval.set_variable_by_name("value", value_for_eval)
266                    .expect("$value declared via compile_with_vars");
267            }) {
268            Ok(r) => r,
269            Err(e) => {
270                errors.push(errors::error(
271                    "cvc-assertion",
272                    format!(
273                        "Failed to evaluate assertion test '{}': {}",
274                        assertion.test, e
275                    ),
276                    None,
277                ));
278                continue;
279            }
280        };
281
282        // Check effective boolean value
283        match effective_boolean_value(&result) {
284            Ok(true) => { /* assertion passed */ }
285            Ok(false) => {
286                errors.push(errors::error(
287                    "cvc-assertion",
288                    format!("Assertion '{}' failed", assertion.test),
289                    None,
290                ));
291            }
292            Err(e) => {
293                errors.push(errors::error(
294                    "cvc-assertion",
295                    format!(
296                        "Failed to compute boolean value for assertion '{}': {}",
297                        assertion.test, e
298                    ),
299                    None,
300                ));
301            }
302        }
303    }
304
305    errors
306}
307
308// ---------------------------------------------------------------------------
309// Tests
310// ---------------------------------------------------------------------------
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use crate::pipeline::load_and_process_schema;
316
317    fn load_schema(xsd: &str) -> SchemaSet {
318        let mut schema_set = SchemaSet::xsd11();
319        load_and_process_schema(xsd.as_bytes(), "test.xsd", &mut schema_set, None)
320            .expect("failed to load schema");
321        schema_set
322    }
323
324    /// Find the first complex type key in the schema set by name.
325    fn find_ct_key(schema_set: &SchemaSet, name: &str) -> ComplexTypeKey {
326        let name_id = schema_set.name_table.add(name);
327        for (key, ct) in &schema_set.arenas.complex_types {
328            if ct.name == Some(name_id) {
329                return key;
330            }
331        }
332        panic!("Complex type '{}' not found", name);
333    }
334
335    #[test]
336    fn test_has_inherited_assertions_none() {
337        let schema_set = load_schema(
338            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
339                <xs:complexType name="plain">
340                    <xs:sequence>
341                        <xs:element name="x" type="xs:string"/>
342                    </xs:sequence>
343                </xs:complexType>
344            </xs:schema>"#,
345        );
346        let key = find_ct_key(&schema_set, "plain");
347        assert!(!has_inherited_assertions(key, &schema_set.arenas));
348    }
349
350    #[test]
351    fn test_has_inherited_assertions_own() {
352        // xs:assert as direct child of complexType with attribute-only content
353        let schema_set = load_schema(
354            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
355                <xs:complexType name="withAssert">
356                    <xs:attribute name="val" type="xs:integer"/>
357                    <xs:assert test="@val >= 0"/>
358                </xs:complexType>
359            </xs:schema>"#,
360        );
361        let key = find_ct_key(&schema_set, "withAssert");
362        assert!(has_inherited_assertions(key, &schema_set.arenas));
363    }
364
365    #[test]
366    fn test_has_inherited_assertions_from_base() {
367        let schema_set = load_schema(
368            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
369                <xs:complexType name="base">
370                    <xs:attribute name="val" type="xs:integer"/>
371                    <xs:assert test="@val >= 0"/>
372                </xs:complexType>
373                <xs:complexType name="derived">
374                    <xs:complexContent>
375                        <xs:restriction base="base">
376                            <xs:attribute name="val" type="xs:integer"/>
377                        </xs:restriction>
378                    </xs:complexContent>
379                </xs:complexType>
380            </xs:schema>"#,
381        );
382        let key = find_ct_key(&schema_set, "derived");
383        assert!(has_inherited_assertions(key, &schema_set.arenas));
384    }
385
386    #[test]
387    fn test_collect_inherited_assertions_ordering() {
388        let schema_set = load_schema(
389            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
390                <xs:complexType name="base">
391                    <xs:attribute name="val" type="xs:integer"/>
392                    <xs:assert test="@val >= 0"/>
393                </xs:complexType>
394                <xs:complexType name="derived">
395                    <xs:complexContent>
396                        <xs:restriction base="base">
397                            <xs:attribute name="val" type="xs:integer"/>
398                            <xs:assert test="@val &lt; 100"/>
399                        </xs:restriction>
400                    </xs:complexContent>
401                </xs:complexType>
402            </xs:schema>"#,
403        );
404        let derived_key = find_ct_key(&schema_set, "derived");
405        let base_key = find_ct_key(&schema_set, "base");
406        let assertions = collect_inherited_assertions(derived_key, &schema_set.arenas);
407
408        // Base-first ordering: base assertion comes first
409        assert_eq!(assertions.len(), 2);
410        assert_eq!(
411            assertions[0].1, base_key,
412            "first assertion should be from base"
413        );
414        assert_eq!(
415            assertions[1].1, derived_key,
416            "second assertion should be from derived"
417        );
418        assert!(assertions[0].0.test.contains(">= 0"));
419        assert!(assertions[1].0.test.contains("< 100"));
420    }
421
422    #[test]
423    fn test_collect_inherited_assertions_no_assertions() {
424        let schema_set = load_schema(
425            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
426                <xs:complexType name="plain">
427                    <xs:sequence>
428                        <xs:element name="x" type="xs:string"/>
429                    </xs:sequence>
430                </xs:complexType>
431            </xs:schema>"#,
432        );
433        let key = find_ct_key(&schema_set, "plain");
434        let assertions = collect_inherited_assertions(key, &schema_set.arenas);
435        assert!(assertions.is_empty());
436    }
437}