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}