Skip to main content

xsd_schema/document/
typed_builder.rs

1//! Schema-aware document builder.
2//!
3//! [`build_typed_document`] constructs a [`BufferDocument`] while interleaving
4//! validation calls so that each element and attribute node carries a
5//! [`NodeSchemaBinding`] (type key, declaration keys, content type).
6
7use std::io::BufRead;
8
9use bumpalo::Bump;
10
11use crate::namespace::table::XML_NAMESPACE;
12use crate::parser::location::SourceSpan;
13use crate::schema::SchemaSet;
14use crate::validation::errors::ValidationError;
15use crate::validation::info::{SchemaInfo, ValidationFlags};
16use crate::validation::quick_xml_driver::{
17    drive_quick_xml_with, AttributeView, DriveWithError, ElementStartView, EndElementInfo,
18    EndOfAttributesView, TextKind, ValidationEventHandler,
19};
20use crate::validation::validator::{ValidationSink, ValidationWarning};
21use crate::validation::SchemaValidator;
22
23use super::buffer::BufferDocument;
24use super::builder::BufferDocumentBuilder;
25use super::error::BufferDocumentError;
26use super::type_remap::NodeSchemaBinding;
27use super::BufferDocumentOptions;
28
29// ── SilentValidationSink ──────────────────────────────────────────────
30
31/// A [`ValidationSink`] that silently discards all errors and warnings.
32///
33/// Used by [`build_typed_document`] when the caller only needs schema
34/// bindings on nodes and does not care about validation diagnostics.
35pub struct SilentValidationSink;
36
37impl ValidationSink for SilentValidationSink {
38    fn on_error(&mut self, _error: ValidationError) {}
39    fn on_warning(&mut self, _warning: ValidationWarning) {}
40}
41
42// ── Handler ───────────────────────────────────────────────────────────
43
44/// Handler that mirrors validator events into a [`BufferDocumentBuilder`].
45///
46/// Holds the per-element bookkeeping the design doc separates out: the
47/// element-ref stack (so text/end events can correlate to the open element),
48/// the single-slot scratch for the current attribute being processed, the
49/// per-element queue of CTA-deferred attribute refs, and the source-span
50/// tracking pieces.
51struct TypedBuilderHandler<'b, 'a> {
52    builder: &'b mut BufferDocumentBuilder<'a>,
53    track: bool,
54    pending_start_offset: Option<usize>,
55    /// Currently open elements' refs. Pushed at `before_element`, popped at
56    /// `after_end_element`.
57    elem_ref_stack: Vec<u32>,
58    /// (elem_ref, start_byte) for each currently open element when
59    /// `track` is on. Popped at `on_element_end_offset` to set the span.
60    pending_spans: Vec<(u32, usize)>,
61    /// Single-slot scratch for the attribute currently between
62    /// `before_attribute` and `after_attribute`.
63    current_attr_ref: Option<u32>,
64    /// Per-element queue of attribute refs whose binding is deferred until
65    /// CTA reselection. Drained in `after_end_of_attributes`.
66    #[cfg(feature = "xsd11")]
67    deferred_attr_refs: Vec<u32>,
68    /// Element-decl key for the currently open element (used so we can
69    /// preserve `element_decl` when refreshing the binding after CTA).
70    element_decl_stack: Vec<Option<crate::ids::ElementKey>>,
71}
72
73impl<'b, 'a> TypedBuilderHandler<'b, 'a> {
74    fn new(builder: &'b mut BufferDocumentBuilder<'a>) -> Self {
75        let track = builder.track_source_locations();
76        Self {
77            builder,
78            track,
79            pending_start_offset: None,
80            elem_ref_stack: Vec::new(),
81            pending_spans: Vec::new(),
82            current_attr_ref: None,
83            #[cfg(feature = "xsd11")]
84            deferred_attr_refs: Vec::new(),
85            element_decl_stack: Vec::new(),
86        }
87    }
88}
89
90impl<'b, 'a> ValidationEventHandler for TypedBuilderHandler<'b, 'a> {
91    type Error = BufferDocumentError;
92
93    fn on_element_start_offset(&mut self, byte_pos: usize) -> Result<(), Self::Error> {
94        if self.track {
95            self.pending_start_offset = Some(byte_pos);
96        }
97        Ok(())
98    }
99
100    fn before_element(&mut self, view: ElementStartView<'_>) -> Result<(), Self::Error> {
101        let elem_ref = self.builder.start_element(
102            view.local_name,
103            view.namespace_uri,
104            view.prefix,
105            view.namespace_decls,
106        )?;
107        self.elem_ref_stack.push(elem_ref);
108        if self.track {
109            if let Some(start) = self.pending_start_offset.take() {
110                self.pending_spans.push((elem_ref, start));
111            }
112        }
113        Ok(())
114    }
115
116    fn after_element(
117        &mut self,
118        _view: ElementStartView<'_>,
119        info: &SchemaInfo,
120    ) -> Result<(), Self::Error> {
121        let elem_ref = *self
122            .elem_ref_stack
123            .last()
124            .expect("after_element with empty stack");
125        self.element_decl_stack.push(info.element_decl);
126        if let Some(tk) = info.schema_type {
127            let binding = NodeSchemaBinding {
128                type_key: tk,
129                element_decl: info.element_decl,
130                attribute_decl: None,
131                content_type: info.content_type,
132            };
133            self.builder.set_node_binding(elem_ref, binding)?;
134        }
135        if info.is_nil {
136            self.builder.set_nil(elem_ref);
137        }
138        Ok(())
139    }
140
141    fn before_attribute(&mut self, view: AttributeView<'_>) -> Result<(), Self::Error> {
142        let attr_ref = self.builder.attribute(
143            view.local_name,
144            view.namespace_uri,
145            view.prefix,
146            view.value,
147        )?;
148        self.current_attr_ref = Some(attr_ref);
149        Ok(())
150    }
151
152    fn after_attribute(
153        &mut self,
154        view: AttributeView<'_>,
155        info: &SchemaInfo,
156    ) -> Result<(), Self::Error> {
157        let attr_ref = self
158            .current_attr_ref
159            .take()
160            .expect("after_attribute without matching before_attribute");
161        if let Some(tk) = info.schema_type {
162            let binding = NodeSchemaBinding {
163                type_key: tk,
164                element_decl: None,
165                attribute_decl: info.attribute_decl,
166                content_type: None,
167            };
168            self.builder.set_node_binding(attr_ref, binding)?;
169        }
170        if view.local_name == "id" && view.namespace_uri == XML_NAMESPACE {
171            let owner_elem = *self
172                .elem_ref_stack
173                .last()
174                .expect("xml:id without an open element");
175            self.builder.register_xml_id(view.value, owner_elem)?;
176        }
177        #[cfg(feature = "xsd11")]
178        if info.deferred_by_cta {
179            self.deferred_attr_refs.push(attr_ref);
180        }
181        Ok(())
182    }
183
184    fn after_end_of_attributes(
185        &mut self,
186        view: EndOfAttributesView<'_>,
187    ) -> Result<(), Self::Error> {
188        self.builder.end_of_attributes();
189        let elem_ref = *self
190            .elem_ref_stack
191            .last()
192            .expect("after_end_of_attributes without an open element");
193        let element_decl = *self
194            .element_decl_stack
195            .last()
196            .expect("after_end_of_attributes without an open element");
197
198        if let Some(tk) = view.info.schema_type {
199            let binding = NodeSchemaBinding {
200                type_key: tk,
201                element_decl,
202                attribute_decl: None,
203                content_type: view.info.content_type,
204            };
205            self.builder.set_node_binding(elem_ref, binding)?;
206        }
207
208        #[cfg(feature = "xsd11")]
209        {
210            let deferred_refs = std::mem::take(&mut self.deferred_attr_refs);
211            if deferred_refs.len() != view.deferred_attribute_results.len() {
212                return Err(BufferDocumentError::InternalError(
213                    "deferred attribute count mismatch".into(),
214                ));
215            }
216            for (attr_ref, attr_info) in deferred_refs
217                .iter()
218                .zip(view.deferred_attribute_results.iter())
219            {
220                if let Some(tk) = attr_info.schema_type {
221                    let binding = NodeSchemaBinding {
222                        type_key: tk,
223                        element_decl: None,
224                        attribute_decl: attr_info.attribute_decl,
225                        content_type: None,
226                    };
227                    self.builder.set_node_binding(*attr_ref, binding)?;
228                }
229            }
230        }
231
232        Ok(())
233    }
234
235    fn after_end_element(
236        &mut self,
237        _info: &EndElementInfo,
238        _depth: usize,
239    ) -> Result<(), Self::Error> {
240        self.builder.end_element()?;
241        self.elem_ref_stack.pop();
242        self.element_decl_stack.pop();
243        Ok(())
244    }
245
246    fn on_element_end_offset(&mut self, byte_pos: usize) -> Result<(), Self::Error> {
247        if self.track {
248            if let Some((elem_ref, start)) = self.pending_spans.pop() {
249                self.builder
250                    .set_source_span(elem_ref, SourceSpan::new(start, byte_pos));
251            }
252        }
253        Ok(())
254    }
255
256    fn on_text(&mut self, _kind: TextKind, text: &str) -> Result<(), Self::Error> {
257        if !self.elem_ref_stack.is_empty() {
258            self.builder.text(text);
259        }
260        Ok(())
261    }
262
263    fn on_comment(&mut self, text: &str) -> Result<(), Self::Error> {
264        self.builder.comment(text)?;
265        Ok(())
266    }
267
268    fn on_processing_instruction(
269        &mut self,
270        target: &str,
271        data: &str,
272    ) -> Result<(), Self::Error> {
273        self.builder.processing_instruction(target, data)?;
274        Ok(())
275    }
276}
277
278// ── build_typed_document ──────────────────────────────────────────────
279
280/// Build a [`BufferDocument`] with schema bindings on every element and attribute.
281///
282/// This function mirrors [`BufferDocumentBuilder::build`] but interleaves
283/// [`SchemaValidator`] calls so that the resulting document carries
284/// [`NodeSchemaBinding`] entries for typed-value and schema-type queries.
285///
286/// Validation errors are silently discarded via [`SilentValidationSink`].
287/// If you need to collect validation errors, use the lower-level push API
288/// with a custom sink instead.
289pub fn build_typed_document<'a, R: BufRead>(
290    reader: R,
291    arena: &'a Bump,
292    schema_set: &'a SchemaSet,
293    options: BufferDocumentOptions,
294) -> Result<BufferDocument<'a>, BufferDocumentError> {
295    let mut builder =
296        BufferDocumentBuilder::new(arena, &schema_set.name_table, Some(schema_set), options)?;
297
298    let validator = SchemaValidator::new(schema_set, ValidationFlags::default());
299    let mut runtime = validator.start_run(SilentValidationSink);
300
301    {
302        let mut handler = TypedBuilderHandler::new(&mut builder);
303        drive_quick_xml_with(reader, &mut runtime, schema_set, &mut handler).map_err(|e| {
304            match e {
305                DriveWithError::Parse(e) => BufferDocumentError::Parse(e),
306                DriveWithError::Utf8(e) => BufferDocumentError::Utf8(e),
307                DriveWithError::UnboundPrefix(p) => BufferDocumentError::UnboundPrefix(p),
308                DriveWithError::UnexpectedEof { depth } => {
309                    BufferDocumentError::InternalError(format!(
310                        "unexpected EOF: {} element(s) still open",
311                        depth
312                    ))
313                }
314                DriveWithError::Hook(e) => e,
315            }
316        })?;
317    }
318
319    let _ = runtime.end_validation();
320    builder.finalize()
321}
322
323// ── Tests ─────────────────────────────────────────────────────────────
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use crate::ids::TypeKey;
329    use crate::navigator::{DomNavigator, TypedValue};
330    use crate::pipeline::load_and_process_schema;
331    use crate::validation::info::ContentType;
332
333    fn load_schema(xsd: &str) -> SchemaSet {
334        let mut schema_set = SchemaSet::xsd11();
335        load_and_process_schema(xsd.as_bytes(), "test.xsd", &mut schema_set, None)
336            .expect("failed to load schema");
337        schema_set
338    }
339
340    fn build_doc<'a>(xml: &str, arena: &'a Bump, schema_set: &'a SchemaSet) -> BufferDocument<'a> {
341        build_typed_document(
342            xml.as_bytes(),
343            arena,
344            schema_set,
345            BufferDocumentOptions::default(),
346        )
347        .expect("failed to build typed document")
348    }
349
350    // ── Test 1: schema_type() / element_type_key() work ──────────────
351
352    #[test]
353    fn typed_document_has_schema_bindings() {
354        let schema_set = load_schema(
355            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
356                <xs:element name="root" type="xs:string"/>
357            </xs:schema>"#,
358        );
359        let arena = Bump::new();
360        let doc = build_doc("<root>hello</root>", &arena, &schema_set);
361
362        let mut nav = doc.create_navigator();
363        assert!(nav.move_to_first_child()); // root element
364        assert!(nav.element_type_key().is_some());
365        assert!(matches!(nav.element_type_key(), Some(TypeKey::Simple(_))));
366    }
367
368    // ── Test 2: typed_value() for xs:integer attribute ────────────────
369
370    #[test]
371    fn typed_value_integer_attribute() {
372        let schema_set = load_schema(
373            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
374                <xs:element name="root">
375                    <xs:complexType>
376                        <xs:attribute name="count" type="xs:integer"/>
377                    </xs:complexType>
378                </xs:element>
379            </xs:schema>"#,
380        );
381        let arena = Bump::new();
382        let doc = build_doc(r#"<root count="42"/>"#, &arena, &schema_set);
383
384        let mut nav = doc.create_navigator();
385        assert!(nav.move_to_first_child()); // root element
386        assert!(nav.move_to_first_attribute());
387        assert!(nav.element_type_key().is_some());
388        let tv = nav.typed_value();
389        assert!(
390            matches!(tv, TypedValue::Value(_)),
391            "attribute should have typed value"
392        );
393    }
394
395    // ── Test 3: typed_value() for TextOnly element ────────────────────
396
397    #[test]
398    fn typed_value_text_only_element() {
399        let schema_set = load_schema(
400            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
401                <xs:element name="root" type="xs:integer"/>
402            </xs:schema>"#,
403        );
404        let arena = Bump::new();
405        let doc = build_doc("<root>123</root>", &arena, &schema_set);
406
407        let mut nav = doc.create_navigator();
408        assert!(nav.move_to_first_child()); // root element
409        let tv = nav.typed_value();
410        assert!(
411            matches!(tv, TypedValue::Value(_)),
412            "simple-typed element should have typed value"
413        );
414    }
415
416    // ── Test 4: typed_value() returns Absent for ElementOnly/Mixed ────
417
418    #[test]
419    fn typed_value_absent_for_element_only() {
420        let schema_set = load_schema(
421            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
422                <xs:element name="root">
423                    <xs:complexType>
424                        <xs:sequence>
425                            <xs:element name="child" type="xs:string"/>
426                        </xs:sequence>
427                    </xs:complexType>
428                </xs:element>
429            </xs:schema>"#,
430        );
431        let arena = Bump::new();
432        let doc = build_doc("<root><child>hello</child></root>", &arena, &schema_set);
433
434        let mut nav = doc.create_navigator();
435        assert!(nav.move_to_first_child()); // root element
436        assert!(nav.element_type_key().is_some());
437        assert_eq!(
438            nav.typed_value(),
439            TypedValue::Absent,
440            "ElementOnly complex type should produce Absent"
441        );
442    }
443
444    // ── Test 5: complex type with simpleContent → typed_value works ──
445
446    #[test]
447    fn typed_value_simple_content() {
448        let schema_set = load_schema(
449            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
450                <xs:element name="root">
451                    <xs:complexType>
452                        <xs:simpleContent>
453                            <xs:extension base="xs:integer">
454                                <xs:attribute name="unit" type="xs:string"/>
455                            </xs:extension>
456                        </xs:simpleContent>
457                    </xs:complexType>
458                </xs:element>
459            </xs:schema>"#,
460        );
461        let arena = Bump::new();
462        let doc = build_doc(r#"<root unit="kg">42</root>"#, &arena, &schema_set);
463
464        let mut nav = doc.create_navigator();
465        assert!(nav.move_to_first_child()); // root element
466        assert!(matches!(nav.element_type_key(), Some(TypeKey::Complex(_))));
467        let binding = nav.schema_binding().unwrap();
468        assert_eq!(
469            binding.content_type,
470            Some(ContentType::TextOnly),
471            "simpleContent should have TextOnly content type"
472        );
473        let tv = nav.typed_value();
474        assert!(
475            matches!(tv, TypedValue::Value(_)),
476            "simpleContent element should have typed_value"
477        );
478    }
479
480    // ── Test 6: untyped document → binding_index=0, typed_value=Untyped
481
482    #[test]
483    fn untyped_document_no_bindings() {
484        let schema_set = load_schema(
485            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
486                <xs:element name="other" type="xs:string"/>
487            </xs:schema>"#,
488        );
489        let arena = Bump::new();
490        // Element "root" not declared in schema — unresolved
491        let doc = build_doc("<root>hello</root>", &arena, &schema_set);
492
493        let mut nav = doc.create_navigator();
494        assert!(nav.move_to_first_child()); // root element
495                                            // Unknown element should still build, just no type binding
496        assert_eq!(nav.typed_value(), TypedValue::Untyped);
497    }
498
499    // ── Test 7: xsi:type override ─────────────────────────────────────
500
501    #[test]
502    fn xsi_type_override() {
503        let schema_set = load_schema(
504            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
505                <xs:element name="root" type="xs:string"/>
506            </xs:schema>"#,
507        );
508        let arena = Bump::new();
509        let doc = build_doc(
510            r#"<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
511                   xmlns:xs="http://www.w3.org/2001/XMLSchema"
512                   xsi:type="xs:integer">42</root>"#,
513            &arena,
514            &schema_set,
515        );
516
517        let mut nav = doc.create_navigator();
518        assert!(nav.move_to_first_child()); // root element
519        assert!(nav.element_type_key().is_some());
520        // The type should be resolved (either xs:integer or the declared type)
521        let tv = nav.typed_value();
522        assert!(
523            matches!(tv, TypedValue::Value(_)),
524            "xsi:type override should produce typed value"
525        );
526    }
527
528    // ── Test 8: xsi:nil → IS_NIL, typed_value() returns Nilled ───────
529
530    #[test]
531    fn xsi_nil_sets_flag() {
532        let schema_set = load_schema(
533            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
534                <xs:element name="root" type="xs:string" nillable="true"/>
535            </xs:schema>"#,
536        );
537        let arena = Bump::new();
538        let doc = build_doc(
539            r#"<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
540                   xsi:nil="true"/>"#,
541            &arena,
542            &schema_set,
543        );
544
545        let mut nav = doc.create_navigator();
546        assert!(nav.move_to_first_child()); // root element
547        assert_eq!(
548            nav.typed_value(),
549            TypedValue::Nilled,
550            "nil element should return Nilled"
551        );
552    }
553
554    // ── Test 9: NameTable sharing ────────────────────────────────────
555
556    #[test]
557    fn name_table_sharing() {
558        let schema_set = load_schema(
559            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
560                <xs:element name="root" type="xs:string"/>
561            </xs:schema>"#,
562        );
563        let arena = Bump::new();
564        let doc = build_doc("<root>hello</root>", &arena, &schema_set);
565
566        // Builder and validator share schema_set.name_table
567        assert!(std::ptr::eq(doc.names(), &schema_set.name_table));
568    }
569
570    // ── Test 10: element_type_key() returns both Simple and Complex ──
571
572    #[test]
573    fn element_type_key_simple_and_complex() {
574        let schema_set = load_schema(
575            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
576                <xs:element name="root">
577                    <xs:complexType>
578                        <xs:sequence>
579                            <xs:element name="val" type="xs:integer"/>
580                        </xs:sequence>
581                    </xs:complexType>
582                </xs:element>
583            </xs:schema>"#,
584        );
585        let arena = Bump::new();
586        let doc = build_doc("<root><val>10</val></root>", &arena, &schema_set);
587
588        let mut nav = doc.create_navigator();
589        assert!(nav.move_to_first_child()); // root element
590        assert!(
591            matches!(nav.element_type_key(), Some(TypeKey::Complex(_))),
592            "root should have Complex type key"
593        );
594
595        // Navigate to <val>
596        assert!(nav.move_to_first_child());
597        assert!(
598            matches!(nav.element_type_key(), Some(TypeKey::Simple(_))),
599            "val should have Simple type key"
600        );
601    }
602
603    // ── Test 11: default value on element → typed_value uses default ─
604
605    #[test]
606    fn default_value_on_empty_element() {
607        let schema_set = load_schema(
608            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
609                <xs:element name="root" type="xs:integer" default="99"/>
610            </xs:schema>"#,
611        );
612        let arena = Bump::new();
613        // Empty element — no text content, should use default "99"
614        let doc = build_doc("<root/>", &arena, &schema_set);
615
616        let mut nav = doc.create_navigator();
617        assert!(nav.move_to_first_child()); // root element
618        let tv = nav.typed_value();
619        assert!(
620            matches!(tv, TypedValue::Value(_)),
621            "empty element with default should produce typed value"
622        );
623    }
624
625    // ── Test 11b: fixed value on element → typed_value uses fixed ──
626
627    #[test]
628    fn fixed_value_on_empty_element() {
629        let schema_set = load_schema(
630            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
631                <xs:element name="root" type="xs:integer" fixed="42"/>
632            </xs:schema>"#,
633        );
634        let arena = Bump::new();
635        // Empty element — no text content, should use fixed "42"
636        let doc = build_doc("<root/>", &arena, &schema_set);
637
638        let mut nav = doc.create_navigator();
639        assert!(nav.move_to_first_child()); // root element
640        let tv = nav.typed_value();
641        assert!(
642            matches!(tv, TypedValue::Value(_)),
643            "empty element with fixed value should produce typed value, got: {:?}",
644            tv
645        );
646    }
647
648    // ── Test 12: source span tracking works ───────────────────────────
649
650    #[test]
651    fn source_span_tracking() {
652        let schema_set = load_schema(
653            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
654                <xs:element name="root" type="xs:string"/>
655            </xs:schema>"#,
656        );
657        let arena = Bump::new();
658        let doc = build_typed_document(
659            "<root>hello</root>".as_bytes(),
660            &arena,
661            &schema_set,
662            BufferDocumentOptions::full(), // track_source_locations = true
663        )
664        .unwrap();
665
666        // Source spans should be populated
667        assert!(
668            !doc.source_spans.is_empty(),
669            "source spans should be recorded with full() options"
670        );
671    }
672
673    // ── Test 13: comments and PIs preserved in typed document ────────
674
675    #[test]
676    fn comments_and_pis_preserved() {
677        use crate::document::node::NodeType;
678
679        let schema_set = load_schema(
680            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
681                <xs:element name="root">
682                    <xs:complexType>
683                        <xs:sequence>
684                            <xs:element name="child" type="xs:string"/>
685                        </xs:sequence>
686                    </xs:complexType>
687                </xs:element>
688            </xs:schema>"#,
689        );
690        let arena = Bump::new();
691        let xml = "<root><!-- before --><?p1 d1?><child>hi</child><!-- after --></root>";
692        let doc = build_typed_document(
693            xml.as_bytes(),
694            &arena,
695            &schema_set,
696            BufferDocumentOptions::default(),
697        )
698        .unwrap();
699
700        let mut comment_count = 0usize;
701        let mut pi_count = 0usize;
702        for i in 0..doc.nodes.len() {
703            match doc.nodes.get(i).node_type() {
704                NodeType::Comment => comment_count += 1,
705                NodeType::ProcessingInstruction => pi_count += 1,
706                _ => {}
707            }
708        }
709        assert_eq!(comment_count, 2, "two comments should be preserved");
710        assert_eq!(pi_count, 1, "one PI should be preserved");
711    }
712
713    // ── Fragment mode tests ──────────────────────────────────────────────
714
715    #[test]
716    fn fragment_set_node_binding_typed_value() {
717        let schema_set = load_schema(
718            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
719                <xs:element name="root" type="xs:integer"/>
720            </xs:schema>"#,
721        );
722        let arena = Bump::new();
723        let doc = build_typed_document(
724            "<root>42</root>".as_bytes(),
725            &arena,
726            &schema_set,
727            BufferDocumentOptions::fragment(),
728        )
729        .unwrap();
730
731        let mut nav = doc.create_navigator();
732        assert!(nav.move_to_first_child()); // root element
733        assert!(nav.element_type_key().is_some());
734        let tv = nav.typed_value();
735        assert!(
736            matches!(tv, TypedValue::Value(_)),
737            "fragment typed_value should resolve, got: {:?}",
738            tv
739        );
740    }
741
742    #[test]
743    fn fragment_schema_set_propagation() {
744        let schema_set = load_schema(
745            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
746                <xs:element name="root" type="xs:string"/>
747            </xs:schema>"#,
748        );
749        let arena = Bump::new();
750        let doc = build_typed_document(
751            "<root>hello</root>".as_bytes(),
752            &arena,
753            &schema_set,
754            BufferDocumentOptions::fragment(),
755        )
756        .unwrap();
757
758        assert!(
759            doc.schema_set().is_some(),
760            "fragment document should carry schema_set reference"
761        );
762    }
763
764    // ── CTA deferred attribute binding tests (XSD 1.1) ──────────────
765
766    #[cfg(feature = "xsd11")]
767    mod cta_deferred_bindings {
768        use super::*;
769
770        /// Schema where CTA switches type based on @kind, and the selected type
771        /// declares an attribute with a specific type.
772        const CTA_ATTR_SCHEMA: &str = r#"
773            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
774                <xs:complexType name="intType">
775                    <xs:attribute name="kind" type="xs:string"/>
776                    <xs:attribute name="val" type="xs:integer"/>
777                </xs:complexType>
778                <xs:complexType name="strType">
779                    <xs:attribute name="kind" type="xs:string"/>
780                    <xs:attribute name="val" type="xs:string"/>
781                </xs:complexType>
782                <xs:element name="data">
783                    <xs:complexType>
784                        <xs:attribute name="kind" type="xs:string"/>
785                        <xs:attribute name="val" type="xs:string"/>
786                    </xs:complexType>
787                    <xs:alternative test="@kind='int'" type="intType"/>
788                    <xs:alternative test="@kind='str'" type="strType"/>
789                </xs:element>
790            </xs:schema>"#;
791
792        #[test]
793        fn cta_selected_type_binds_attribute() {
794            let schema_set = load_schema(CTA_ATTR_SCHEMA);
795            let arena = Bump::new();
796            let doc = build_doc(r#"<data kind="int" val="42"/>"#, &arena, &schema_set);
797
798            let mut nav = doc.create_navigator();
799            assert!(nav.move_to_first_child()); // <data>
800                                                // Element should have intType binding
801            assert!(nav.element_type_key().is_some());
802
803            // Navigate to 'val' attribute — should have xs:integer type from intType
804            assert!(nav.move_to_first_attribute());
805            // Attributes are in document order: kind, val
806            // Move to second attribute 'val'
807            if nav.local_name() == "kind" {
808                assert!(nav.move_to_next_attribute());
809            }
810            assert_eq!(nav.local_name(), "val");
811            assert!(
812                nav.element_type_key().is_some(),
813                "deferred CTA attribute 'val' should have a type binding"
814            );
815        }
816
817        #[test]
818        fn cta_default_type_binds_attribute() {
819            let schema_set = load_schema(CTA_ATTR_SCHEMA);
820            let arena = Bump::new();
821            // kind='str' selects strType — val should be xs:string
822            let doc = build_doc(r#"<data kind="str" val="hello"/>"#, &arena, &schema_set);
823
824            let mut nav = doc.create_navigator();
825            assert!(nav.move_to_first_child()); // <data>
826            assert!(nav.element_type_key().is_some());
827
828            assert!(nav.move_to_first_attribute());
829            if nav.local_name() == "kind" {
830                assert!(nav.move_to_next_attribute());
831            }
832            assert_eq!(nav.local_name(), "val");
833            assert!(
834                nav.element_type_key().is_some(),
835                "deferred CTA attribute 'val' should have a type binding for strType"
836            );
837        }
838
839        #[test]
840        fn cta_multiple_deferred_attributes_ordering() {
841            // Schema where CTA type has multiple typed attributes
842            let schema = r#"
843                <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
844                    <xs:complexType name="fullType">
845                        <xs:attribute name="kind" type="xs:string"/>
846                        <xs:attribute name="x" type="xs:integer"/>
847                        <xs:attribute name="y" type="xs:integer"/>
848                    </xs:complexType>
849                    <xs:element name="point">
850                        <xs:complexType>
851                            <xs:attribute name="kind" type="xs:string"/>
852                            <xs:attribute name="x" type="xs:string"/>
853                            <xs:attribute name="y" type="xs:string"/>
854                        </xs:complexType>
855                        <xs:alternative test="@kind='full'" type="fullType"/>
856                    </xs:element>
857                </xs:schema>"#;
858
859            let schema_set = load_schema(schema);
860            let arena = Bump::new();
861            let doc = build_doc(r#"<point kind="full" x="10" y="20"/>"#, &arena, &schema_set);
862
863            let mut nav = doc.create_navigator();
864            assert!(nav.move_to_first_child()); // <point>
865
866            // Check all attributes have bindings
867            assert!(nav.move_to_first_attribute());
868            let mut bound_count = 0;
869            loop {
870                if nav.element_type_key().is_some() {
871                    bound_count += 1;
872                }
873                if !nav.move_to_next_attribute() {
874                    break;
875                }
876            }
877            assert!(
878                bound_count >= 3,
879                "all 3 attributes (kind, x, y) should have type bindings, got {}",
880                bound_count
881            );
882        }
883
884        #[test]
885        fn cta_nested_elements_no_cross_leakage() {
886            // Schema with CTA on two nested elements
887            let schema = r#"
888                <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
889                    <xs:complexType name="outerAlt">
890                        <xs:sequence>
891                            <xs:element ref="inner"/>
892                        </xs:sequence>
893                        <xs:attribute name="kind" type="xs:string"/>
894                        <xs:attribute name="outerAttr" type="xs:integer"/>
895                    </xs:complexType>
896                    <xs:complexType name="innerAlt">
897                        <xs:attribute name="kind" type="xs:string"/>
898                        <xs:attribute name="innerAttr" type="xs:integer"/>
899                    </xs:complexType>
900                    <xs:element name="inner">
901                        <xs:complexType>
902                            <xs:attribute name="kind" type="xs:string"/>
903                            <xs:attribute name="innerAttr" type="xs:string"/>
904                        </xs:complexType>
905                        <xs:alternative test="@kind='alt'" type="innerAlt"/>
906                    </xs:element>
907                    <xs:element name="outer">
908                        <xs:complexType>
909                            <xs:sequence>
910                                <xs:element ref="inner"/>
911                            </xs:sequence>
912                            <xs:attribute name="kind" type="xs:string"/>
913                            <xs:attribute name="outerAttr" type="xs:string"/>
914                        </xs:complexType>
915                        <xs:alternative test="@kind='alt'" type="outerAlt"/>
916                    </xs:element>
917                </xs:schema>"#;
918
919            let schema_set = load_schema(schema);
920            let arena = Bump::new();
921            let doc = build_doc(
922                r#"<outer kind="alt" outerAttr="99"><inner kind="alt" innerAttr="42"/></outer>"#,
923                &arena,
924                &schema_set,
925            );
926
927            let mut nav = doc.create_navigator();
928            assert!(nav.move_to_first_child()); // <outer>
929            assert!(
930                nav.element_type_key().is_some(),
931                "outer should have type binding"
932            );
933
934            // Check outerAttr has binding
935            assert!(nav.move_to_first_attribute());
936            let mut found_outer_attr = false;
937            loop {
938                if nav.local_name() == "outerAttr" {
939                    assert!(
940                        nav.element_type_key().is_some(),
941                        "outerAttr should have type binding"
942                    );
943                    found_outer_attr = true;
944                }
945                if !nav.move_to_next_attribute() {
946                    break;
947                }
948            }
949            assert!(found_outer_attr, "should find outerAttr");
950
951            // Navigate to <inner>
952            nav.move_to_parent();
953            assert!(nav.move_to_first_child()); // <inner>
954            assert!(
955                nav.element_type_key().is_some(),
956                "inner should have type binding"
957            );
958
959            // Check innerAttr has binding
960            assert!(nav.move_to_first_attribute());
961            let mut found_inner_attr = false;
962            loop {
963                if nav.local_name() == "innerAttr" {
964                    assert!(
965                        nav.element_type_key().is_some(),
966                        "innerAttr should have type binding"
967                    );
968                    found_inner_attr = true;
969                }
970                if !nav.move_to_next_attribute() {
971                    break;
972                }
973            }
974            assert!(found_inner_attr, "should find innerAttr");
975        }
976
977        #[test]
978        fn cta_simple_type_selection_no_panic() {
979            // CTA selects a simple type — the element then has no attribute
980            // declarations. Deferred attributes should produce empty results
981            // (no bindings) rather than triggering an InternalError.
982            let schema = r#"
983                <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
984                    <xs:element name="data">
985                        <xs:complexType>
986                            <xs:attribute name="kind" type="xs:string"/>
987                            <xs:attribute name="val" type="xs:string"/>
988                        </xs:complexType>
989                        <xs:alternative test="@kind='simple'" type="xs:string"/>
990                    </xs:element>
991                </xs:schema>"#;
992
993            let schema_set = load_schema(schema);
994            let arena = Bump::new();
995            // kind='simple' triggers CTA → xs:string (a simple type).
996            // Both attributes were deferred; revalidation must not panic.
997            let result = build_typed_document(
998                r#"<data kind="simple" val="hello"/>"#.as_bytes(),
999                &arena,
1000                &schema_set,
1001                BufferDocumentOptions::default(),
1002            );
1003            assert!(
1004                result.is_ok(),
1005                "CTA selecting simple type should not cause InternalError, got: {:?}",
1006                result.err()
1007            );
1008        }
1009    }
1010}