Skip to main content

xsd_schema/validation/
context.rs

1//! Per-element validation state and validator state machine
2//!
3//! `ElementValidationState` holds all per-element context that is pushed/popped
4//! as the validator enters/exits elements. `ValidatorState` enforces the correct
5//! sequence of push-API calls.
6
7use std::collections::HashSet;
8
9#[cfg(feature = "xsd11")]
10use std::collections::HashMap;
11
12#[cfg(feature = "xsd11")]
13use crate::ids::AttributeKey;
14use crate::ids::{ElementKey, NameId, TypeKey};
15use crate::types::value::XmlValue;
16
17use super::content::ContentValidatorState;
18use super::info::{ContentProcessing, ContentType, SchemaValidity, TypeSource};
19
20/// An inherited attribute value flowing from an ancestor element (XSD 1.1).
21///
22/// Stored in [`ElementValidationState::inherited_attributes`] and propagated
23/// from parent to child on element open. See §3.3.5.6 *Inherited Attributes*.
24#[cfg(feature = "xsd11")]
25#[derive(Debug, Clone)]
26pub struct InheritedAttributeValue {
27    /// The attribute value (string form)
28    pub value: String,
29    /// The governing attribute declaration key, if known
30    pub attribute_key: Option<AttributeKey>,
31}
32
33/// Per-element state pushed onto the validation stack
34///
35/// Each time `validate_element` is called, a new `ElementValidationState` is
36/// created and pushed. It is popped on `validate_end_element`.
37#[derive(Debug, Clone)]
38pub struct ElementValidationState {
39    /// Local name of the element
40    pub local_name: NameId,
41    /// Namespace URI of the element (None for no-namespace)
42    pub namespace: Option<NameId>,
43    /// Resolved element declaration, if found
44    pub element_decl: Option<ElementKey>,
45    /// Resolved schema type (simple or complex)
46    pub schema_type: Option<TypeKey>,
47    /// Content model state for this element's type
48    pub content_state: ContentValidatorState,
49    /// Content type classification (Empty, TextOnly, ElementOnly, Mixed)
50    pub content_type: Option<ContentType>,
51    /// Whether xsi:nil="true" was specified
52    pub is_nil: bool,
53    /// Whether the element value came from a default declaration
54    pub is_default: bool,
55    /// For union types: the actual member type that matched the value
56    pub member_type: Option<TypeKey>,
57    /// The parsed typed value from simple-type validation
58    pub typed_value: Option<XmlValue>,
59    /// The whitespace-normalized value (PSVI `[schema normalized value]`)
60    pub normalized_value: Option<String>,
61    /// Current validity status
62    pub validity: SchemaValidity,
63    /// Accumulated constraint codes for PSVI `[schema error code]`
64    pub error_codes: Vec<&'static str>,
65    /// True if any child element has `[validation attempted]` != Full
66    pub any_child_not_full: bool,
67    /// True if any child element has `[validation attempted]` != None
68    pub any_child_not_none: bool,
69    /// True if any attribute has `[validation attempted]` != Full
70    pub any_attr_not_full: bool,
71    /// True if any attribute has `[validation attempted]` != None
72    pub any_attr_not_none: bool,
73    /// Whether this element was strictly assessed (§5.2 key-sva)
74    pub strictly_assessed: bool,
75    /// Notation declaration resolved from a NOTATION-typed attribute (§3.14.5)
76    pub notation: Option<crate::ids::NotationKey>,
77    /// Namespace context snapshot for resolving NOTATION QNames during attribute validation
78    pub ns_context: Option<crate::namespace::context::NamespaceContextSnapshot>,
79    /// Unique serial number for this element (monotonically increasing).
80    /// Used for XSD 1.1 ID/IDREF binding: same ID on the same owner element is
81    /// allowed (§3.17.5.2).
82    pub element_serial: u64,
83    /// How to process wildcard-matched content
84    pub process_contents: ContentProcessing,
85    /// Effective base URI for this element (inherited from parent, possibly
86    /// overridden by `xml:base`). Used to resolve relative schema-location
87    /// hints in `xsi:schemaLocation` / `xsi:noNamespaceSchemaLocation`.
88    pub base_uri: String,
89    /// Whether `xml:base` has already been applied on this element.
90    /// Prevents a duplicate `xml:base` attribute from overwriting the valid one.
91    pub base_uri_set_by_xml_base: bool,
92    /// Start index of this element's `xsi:schemaLocation` hints in the runtime buffer.
93    pub schema_location_hint_start: usize,
94    /// Start index of this element's `xsi:noNamespaceSchemaLocation` hints in the runtime buffer.
95    pub no_namespace_schema_location_hint_start: usize,
96    /// Set of (namespace, local_name) pairs for attributes already seen
97    pub seen_attributes: HashSet<(Option<NameId>, NameId)>,
98    /// Whether an ID-typed attribute has already been seen on this element.
99    /// Used to enforce the "at most one ID-type attribute per element" rule
100    /// (XSD 1.0 §3.4.4 / §3.5.6 ct-props-correct.5) at runtime, after
101    /// wildcard-matched globals contribute.
102    pub seen_id_attr: bool,
103    /// Accumulated text content for the element
104    pub text_content: String,
105    /// Whether any text nodes have been seen
106    pub has_text: bool,
107    /// Whether any child element nodes have been seen
108    pub has_element_children: bool,
109    /// How the schema_type was determined
110    pub type_source: Option<TypeSource>,
111    /// Whether CTA selected a type (XSD 1.1)
112    #[cfg(feature = "xsd11")]
113    pub cta_selected: bool,
114    /// Whether this element owns an assertion buffer frame (XSD 1.1)
115    #[cfg(feature = "xsd11")]
116    pub owns_assertion_buffer: bool,
117    /// Whether this element has type alternatives (XSD 1.1)
118    #[cfg(feature = "xsd11")]
119    pub has_type_alternatives: bool,
120    /// Collected attributes for type alternative XPath evaluation (XSD 1.1)
121    #[cfg(feature = "xsd11")]
122    pub collected_attributes: Vec<(Option<NameId>, NameId, String)>,
123    /// Node ref of this element in the assertion fragment document (XSD 1.1).
124    /// Saved during `detect_assertions_on_element` for CTA re-detection.
125    #[cfg(feature = "xsd11")]
126    pub assertion_element_ref: Option<u32>,
127    /// **Incoming** inherited attributes: the PSVI `[inherited attributes]`
128    /// for this element (XSD 1.1 §3.3.5.6, structures.html line 5200).
129    ///
130    /// Snapshot of potentially-inherited attribute values from ancestors,
131    /// frozen at element open. This is what `get_inherited_attributes()`
132    /// returns and what CTA XDM construction reads. Never mutated after
133    /// `push_element()`.
134    #[cfg(feature = "xsd11")]
135    pub incoming_inherited: HashMap<(Option<NameId>, NameId), InheritedAttributeValue>,
136    /// **Outgoing** inherited attributes: the propagation map for this
137    /// element's descendants.
138    ///
139    /// Starts as a clone of `incoming_inherited`, then updated when this
140    /// element has explicit or defaulted inheritable attributes (which
141    /// shadow ancestor values per the nearest-owner rule,
142    /// structures.html line 5205). Children clone this map as their
143    /// `incoming_inherited`.
144    #[cfg(feature = "xsd11")]
145    pub outgoing_inherited: HashMap<(Option<NameId>, NameId), InheritedAttributeValue>,
146}
147
148impl ElementValidationState {
149    /// Create a new element validation state with defaults
150    pub fn new(local_name: NameId, namespace: Option<NameId>) -> Self {
151        ElementValidationState {
152            local_name,
153            namespace,
154            element_decl: None,
155            schema_type: None,
156            content_state: ContentValidatorState::Empty,
157            content_type: None,
158            is_nil: false,
159            is_default: false,
160            member_type: None,
161            typed_value: None,
162            normalized_value: None,
163            validity: SchemaValidity::NotKnown,
164            error_codes: Vec::new(),
165            any_child_not_full: false,
166            any_child_not_none: false,
167            any_attr_not_full: false,
168            any_attr_not_none: false,
169            strictly_assessed: false,
170            notation: None,
171            ns_context: None,
172            element_serial: 0,
173            process_contents: ContentProcessing::Strict,
174            base_uri: String::new(),
175            base_uri_set_by_xml_base: false,
176            schema_location_hint_start: 0,
177            no_namespace_schema_location_hint_start: 0,
178            seen_attributes: HashSet::new(),
179            seen_id_attr: false,
180            text_content: String::new(),
181            has_text: false,
182            has_element_children: false,
183            type_source: None,
184            #[cfg(feature = "xsd11")]
185            cta_selected: false,
186            #[cfg(feature = "xsd11")]
187            owns_assertion_buffer: false,
188            #[cfg(feature = "xsd11")]
189            has_type_alternatives: false,
190            #[cfg(feature = "xsd11")]
191            collected_attributes: Vec::new(),
192            #[cfg(feature = "xsd11")]
193            assertion_element_ref: None,
194            #[cfg(feature = "xsd11")]
195            incoming_inherited: HashMap::new(),
196            #[cfg(feature = "xsd11")]
197            outgoing_inherited: HashMap::new(),
198        }
199    }
200}
201
202/// State machine for the validator's call sequence
203///
204/// Enforces that push-API methods are called in the correct order.
205/// The valid transitions are:
206///
207/// ```text
208/// None → Start → Element → Attribute* → EndOfAttributes → (Text|Whitespace)* → EndElement → ... → Finish
209///                                                          ↑                      |
210///                                                          └── (Element cycle) ────┘
211/// ```
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub enum ValidatorState {
214    /// Initial state, no validation has started
215    None,
216    /// `validate_element` has been called for the root element
217    Start,
218    /// Inside an element (after `validate_element`)
219    Element,
220    /// Processing attributes (after `validate_attribute`)
221    Attribute,
222    /// After `validate_end_of_attributes`
223    EndOfAttributes,
224    /// After `validate_text`
225    Text,
226    /// After `validate_whitespace`
227    Whitespace,
228    /// After `validate_end_element`
229    EndElement,
230    /// After `end_validation` — no further calls allowed
231    Finish,
232}
233
234impl ValidatorState {
235    /// Check if `validate_element` can be called in this state
236    pub fn can_start_element(&self) -> bool {
237        matches!(
238            self,
239            ValidatorState::None
240                | ValidatorState::Start
241                | ValidatorState::EndOfAttributes
242                | ValidatorState::Text
243                | ValidatorState::Whitespace
244                | ValidatorState::EndElement
245        )
246    }
247
248    /// Check if `validate_attribute` can be called in this state
249    pub fn can_validate_attribute(&self) -> bool {
250        matches!(self, ValidatorState::Element | ValidatorState::Attribute)
251    }
252
253    /// Check if `validate_end_of_attributes` can be called in this state
254    pub fn can_end_attributes(&self) -> bool {
255        matches!(self, ValidatorState::Element | ValidatorState::Attribute)
256    }
257
258    /// Check if `validate_text` / `validate_whitespace` can be called in this state
259    pub fn can_validate_text(&self) -> bool {
260        matches!(
261            self,
262            ValidatorState::EndOfAttributes
263                | ValidatorState::Text
264                | ValidatorState::Whitespace
265                | ValidatorState::EndElement
266        )
267    }
268
269    /// Check if `validate_end_element` can be called in this state
270    pub fn can_end_element(&self) -> bool {
271        matches!(
272            self,
273            ValidatorState::EndOfAttributes
274                | ValidatorState::Text
275                | ValidatorState::Whitespace
276                | ValidatorState::EndElement
277        )
278    }
279
280    /// Check if `end_validation` can be called in this state
281    pub fn can_finish(&self) -> bool {
282        matches!(self, ValidatorState::EndElement | ValidatorState::None)
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn test_element_validation_state_defaults() {
292        let state = ElementValidationState::new(NameId(1), None);
293        assert_eq!(state.local_name, NameId(1));
294        assert!(state.namespace.is_none());
295        assert!(state.element_decl.is_none());
296        assert!(state.schema_type.is_none());
297        assert!(state.content_type.is_none());
298        assert!(!state.is_nil);
299        assert!(!state.is_default);
300        assert_eq!(state.validity, SchemaValidity::NotKnown);
301        assert_eq!(state.process_contents, ContentProcessing::Strict);
302        assert!(state.seen_attributes.is_empty());
303        assert!(state.text_content.is_empty());
304        assert!(!state.has_text);
305        assert!(!state.has_element_children);
306        assert!(state.type_source.is_none());
307        #[cfg(feature = "xsd11")]
308        assert!(!state.cta_selected);
309    }
310
311    #[test]
312    fn test_element_validation_state_with_namespace() {
313        let state = ElementValidationState::new(NameId(5), Some(NameId(10)));
314        assert_eq!(state.local_name, NameId(5));
315        assert_eq!(state.namespace, Some(NameId(10)));
316    }
317
318    #[test]
319    fn test_seen_attributes_dedup() {
320        let mut state = ElementValidationState::new(NameId(1), None);
321        let attr = (None, NameId(100));
322        assert!(state.seen_attributes.insert(attr));
323        // Second insert returns false — duplicate
324        assert!(!state.seen_attributes.insert(attr));
325        assert_eq!(state.seen_attributes.len(), 1);
326    }
327
328    #[test]
329    fn test_validator_state_transitions() {
330        // None -> can start element
331        assert!(ValidatorState::None.can_start_element());
332        assert!(!ValidatorState::None.can_validate_attribute());
333        assert!(ValidatorState::None.can_finish());
334
335        // Element -> can validate attribute, can end attributes
336        assert!(ValidatorState::Element.can_validate_attribute());
337        assert!(ValidatorState::Element.can_end_attributes());
338        assert!(!ValidatorState::Element.can_validate_text());
339        assert!(!ValidatorState::Element.can_end_element());
340
341        // Attribute -> can continue attributes, can end attributes
342        assert!(ValidatorState::Attribute.can_validate_attribute());
343        assert!(ValidatorState::Attribute.can_end_attributes());
344
345        // EndOfAttributes -> can have text, children, or end
346        assert!(ValidatorState::EndOfAttributes.can_validate_text());
347        assert!(ValidatorState::EndOfAttributes.can_start_element());
348        assert!(ValidatorState::EndOfAttributes.can_end_element());
349
350        // Text -> can have more text, children, or end
351        assert!(ValidatorState::Text.can_validate_text());
352        assert!(ValidatorState::Text.can_start_element());
353        assert!(ValidatorState::Text.can_end_element());
354
355        // EndElement -> can start sibling or end
356        assert!(ValidatorState::EndElement.can_start_element());
357        assert!(ValidatorState::EndElement.can_end_element());
358        assert!(ValidatorState::EndElement.can_finish());
359
360        // Finish -> nothing allowed
361        assert!(!ValidatorState::Finish.can_start_element());
362        assert!(!ValidatorState::Finish.can_validate_attribute());
363        assert!(!ValidatorState::Finish.can_validate_text());
364        assert!(!ValidatorState::Finish.can_end_element());
365        assert!(!ValidatorState::Finish.can_finish());
366    }
367}