Skip to main content

xsd_schema/validation/
content.rs

1//! Content model state dispatch for instance validation
2//!
3//! Wraps NFA and AllGroup content model states into a unified enum,
4//! providing a common interface for advancing the content model
5//! and checking completion.
6
7use crate::compiler::{
8    term_matches_with_substitution, ActiveStates, AllGroupModel, AllGroupState,
9    ContentModelMatcher, NfaTable, NfaTerm, OpenContentMode as AllGroupOpenContentMode,
10    SubstitutionGroupMap, TermMatchResult,
11};
12use crate::ids::{ElementKey, NameId, TypeKey};
13use crate::schema::model::XsdVersion;
14use crate::types::complex::{
15    not_qnames_exclude, NamespaceConstraint, OpenContentMode as TypesOpenContentMode,
16    ProcessContents,
17};
18
19/// Open content information carried through validation
20#[derive(Debug, Clone)]
21pub struct OpenContentInfo {
22    /// Open content mode
23    pub mode: TypesOpenContentMode,
24    /// Namespace constraint for allowed namespaces
25    pub namespace_constraint: NamespaceConstraint,
26    /// How to process matched content
27    pub process_contents: ProcessContents,
28    /// QNames excluded by notQName (pre-expanded concrete pairs)
29    pub not_qnames: Vec<(Option<NameId>, NameId)>,
30}
31
32/// Information about a matched element from the content model
33#[derive(Debug, Clone, Copy)]
34pub struct ElementMatchInfo {
35    /// The element key from the matching NFA term (if any)
36    pub element_key: Option<ElementKey>,
37    /// The resolved type for local elements (if any)
38    pub resolved_type: Option<TypeKey>,
39    /// Process contents mode from open content wildcard (if matched via open content)
40    pub process_contents: Option<ProcessContents>,
41}
42
43/// Phase of AllGroupExtension composite validation.
44#[cfg(feature = "xsd11")]
45#[derive(Debug, Clone)]
46pub enum AllGroupExtPhase {
47    /// Validating the all-group part (base type particles).
48    AllGroup,
49    /// Transitioned to the NFA extension part.
50    Nfa(ActiveStates),
51}
52
53/// Unified content model validation state
54///
55/// Wraps either an NFA-based or AllGroup-based content model into a single
56/// enum so that `SchemaValidator` can advance the content model without
57/// caring which underlying representation is in use.
58#[derive(Debug, Clone)]
59pub enum ContentValidatorState {
60    /// NFA-based content model (sequence, choice, etc.)
61    Nfa {
62        nfa: NfaTable,
63        active_states: ActiveStates,
64        open_content: Option<OpenContentInfo>,
65    },
66    /// All-group content model (unordered particles)
67    AllGroup {
68        model: AllGroupModel,
69        state: AllGroupState,
70        /// `true` once a suffix open-content element has been matched —
71        /// further declared all-group particles are no longer accepted
72        /// (§3.10.4 suffix semantics: declared content first, then wildcard).
73        suffix_locked: bool,
74    },
75    /// All-group base + NFA extension (XSD 1.1 complex type extension).
76    #[cfg(feature = "xsd11")]
77    AllGroupExtension {
78        model: AllGroupModel,
79        state: AllGroupState,
80        extension_nfa: NfaTable,
81        phase: AllGroupExtPhase,
82    },
83    /// Simple content (text only, no child elements)
84    Simple,
85    /// Empty content (no children or text)
86    Empty,
87}
88
89impl ContentValidatorState {
90    /// Create a content validator state from a compiled content model matcher
91    pub fn from_matcher(matcher: ContentModelMatcher) -> Self {
92        match matcher {
93            ContentModelMatcher::Nfa(nfa) => Self::from_nfa(nfa),
94            ContentModelMatcher::AllGroup(model) => Self::from_all_group(model),
95            ContentModelMatcher::WithOpenContent {
96                nfa,
97                mode,
98                wildcard,
99            } => {
100                let oc = wildcard.map(|w| OpenContentInfo {
101                    mode,
102                    namespace_constraint: w.namespace_constraint,
103                    process_contents: w.process_contents,
104                    not_qnames: w.not_qnames,
105                });
106                let initial = ActiveStates::from_nfa(&nfa);
107                Self::Nfa {
108                    nfa,
109                    active_states: initial,
110                    open_content: oc,
111                }
112            }
113            #[cfg(feature = "xsd11")]
114            ContentModelMatcher::AllGroupExtension {
115                base_model,
116                extension_nfa,
117            } => {
118                let state = base_model.create_state();
119                Self::AllGroupExtension {
120                    model: base_model,
121                    state,
122                    extension_nfa,
123                    phase: AllGroupExtPhase::AllGroup,
124                }
125            }
126        }
127    }
128
129    /// Create a content validator state from an NFA table
130    ///
131    /// Computes the initial epsilon closure from the start state.
132    pub fn from_nfa(nfa: NfaTable) -> Self {
133        let initial = ActiveStates::from_nfa(&nfa);
134        ContentValidatorState::Nfa {
135            nfa,
136            active_states: initial,
137            open_content: None,
138        }
139    }
140
141    /// Create a content validator state from an all-group model
142    pub fn from_all_group(model: AllGroupModel) -> Self {
143        let state = model.create_state();
144        ContentValidatorState::AllGroup {
145            model,
146            state,
147            suffix_locked: false,
148        }
149    }
150
151    /// Advance the content model with a child element
152    ///
153    /// Returns `None` if the element was rejected.
154    /// Returns `Some(ElementMatchInfo)` if accepted, containing the
155    /// `ElementKey` and `resolved_type` from the matching NFA term (if any).
156    pub fn advance_element(
157        &mut self,
158        name: NameId,
159        namespace: Option<NameId>,
160        target_ns: Option<NameId>,
161        xsd_version: XsdVersion,
162        subst_groups: Option<&SubstitutionGroupMap>,
163    ) -> Option<ElementMatchInfo> {
164        match self {
165            ContentValidatorState::Nfa {
166                nfa,
167                active_states,
168                open_content,
169            } => {
170                // First, find the matching element info before advancing
171                let mi = active_states.find_match_info(
172                    nfa,
173                    name,
174                    namespace,
175                    target_ns,
176                    subst_groups,
177                    xsd_version,
178                );
179                let match_info = ElementMatchInfo {
180                    element_key: mi.element_key,
181                    resolved_type: mi.resolved_type,
182                    process_contents: mi.process_contents,
183                };
184
185                let next = match xsd_version {
186                    XsdVersion::V1_0 => active_states.clone().advance(
187                        nfa,
188                        name,
189                        namespace,
190                        target_ns,
191                        subst_groups,
192                        xsd_version,
193                    ),
194                    XsdVersion::V1_1 => active_states.clone().advance_with_priority(
195                        nfa,
196                        name,
197                        namespace,
198                        target_ns,
199                        subst_groups,
200                        xsd_version,
201                    ),
202                };
203                if next.is_empty() {
204                    // No NFA transition matched — try open content wildcard
205                    if let Some(oc) = open_content {
206                        let allow = match oc.mode {
207                            TypesOpenContentMode::Interleave => true,
208                            TypesOpenContentMode::Suffix => active_states.contains_accept(nfa),
209                            TypesOpenContentMode::None => false,
210                        };
211                        if allow
212                            && open_content_allows(
213                                &oc.namespace_constraint,
214                                &oc.not_qnames,
215                                name,
216                                namespace,
217                                target_ns,
218                            )
219                        {
220                            // Suffix mode: lock NFA to accept-only so no declared elements
221                            // are accepted after the first open-content element (§3.10.4 suffix semantics).
222                            if matches!(oc.mode, TypesOpenContentMode::Suffix) {
223                                *active_states = ActiveStates::Simple([nfa.accept_state].into());
224                            }
225                            return Some(ElementMatchInfo {
226                                element_key: None,
227                                resolved_type: None,
228                                process_contents: Some(oc.process_contents),
229                            });
230                        }
231                    }
232                    return None;
233                }
234                *active_states = next;
235                Some(match_info)
236            }
237            ContentValidatorState::AllGroup {
238                model,
239                state,
240                suffix_locked,
241            } => {
242                // Once the suffix open-content section has begun, declared
243                // all-group particles are no longer eligible to match.
244                if !*suffix_locked {
245                    for (i, particle) in model.particles.iter().enumerate() {
246                        if !state.can_accept(model, i) {
247                            continue;
248                        }
249                        let result = term_matches_with_substitution(
250                            &particle.term,
251                            name,
252                            namespace,
253                            target_ns,
254                            subst_groups,
255                            xsd_version,
256                        );
257                        if result == TermMatchResult::Match {
258                            if state.accept(model, i) {
259                                let info = match &particle.term {
260                                    NfaTerm::Element {
261                                        name: term_name,
262                                        namespace: term_ns,
263                                        element_key,
264                                        resolved_type,
265                                    } => {
266                                        if *term_name == name && *term_ns == namespace {
267                                            // Direct match
268                                            ElementMatchInfo {
269                                                element_key: *element_key,
270                                                resolved_type: *resolved_type,
271                                                process_contents: None,
272                                            }
273                                        } else {
274                                            // Substitution match — let runtime resolve
275                                            ElementMatchInfo {
276                                                element_key: None,
277                                                resolved_type: None,
278                                                process_contents: None,
279                                            }
280                                        }
281                                    }
282                                    NfaTerm::Wildcard {
283                                        process_contents, ..
284                                    } => ElementMatchInfo {
285                                        element_key: None,
286                                        resolved_type: None,
287                                        process_contents: Some(*process_contents),
288                                    },
289                                };
290                                return Some(info);
291                            }
292                            return None;
293                        }
294                    }
295                }
296                // No declared particle matched (or suffix lock engaged) —
297                // try open content wildcard.
298                if let Some(oc) = &model.open_content {
299                    let allow = match oc.mode {
300                        AllGroupOpenContentMode::Interleave => true,
301                        AllGroupOpenContentMode::Suffix => state.is_satisfied(model),
302                        AllGroupOpenContentMode::None => false,
303                    };
304                    if allow
305                        && open_content_allows(
306                            &oc.namespace_constraint,
307                            &oc.not_qnames,
308                            name,
309                            namespace,
310                            target_ns,
311                        )
312                    {
313                        // §3.10.4 suffix semantics: once a suffix wildcard
314                        // element matches, no declared all-group particle may
315                        // match again.
316                        if matches!(oc.mode, AllGroupOpenContentMode::Suffix) {
317                            *suffix_locked = true;
318                        }
319                        return Some(ElementMatchInfo {
320                            element_key: None,
321                            resolved_type: None,
322                            process_contents: Some(oc.process_contents),
323                        });
324                    }
325                }
326                None
327            }
328            #[cfg(feature = "xsd11")]
329            ContentValidatorState::AllGroupExtension {
330                model,
331                state,
332                extension_nfa,
333                phase,
334            } => {
335                match phase {
336                    AllGroupExtPhase::AllGroup => {
337                        // Try to match against all-group particles first
338                        for (i, particle) in model.particles.iter().enumerate() {
339                            if !state.can_accept(model, i) {
340                                continue;
341                            }
342                            let result = term_matches_with_substitution(
343                                &particle.term,
344                                name,
345                                namespace,
346                                target_ns,
347                                subst_groups,
348                                xsd_version,
349                            );
350                            if result == TermMatchResult::Match {
351                                if state.accept(model, i) {
352                                    let info = match &particle.term {
353                                        NfaTerm::Element {
354                                            name: term_name,
355                                            namespace: term_ns,
356                                            element_key,
357                                            resolved_type,
358                                        } => {
359                                            if *term_name == name && *term_ns == namespace {
360                                                ElementMatchInfo {
361                                                    element_key: *element_key,
362                                                    resolved_type: *resolved_type,
363                                                    process_contents: None,
364                                                }
365                                            } else {
366                                                // Substitution match — let runtime resolve
367                                                ElementMatchInfo {
368                                                    element_key: None,
369                                                    resolved_type: None,
370                                                    process_contents: None,
371                                                }
372                                            }
373                                        }
374                                        NfaTerm::Wildcard {
375                                            process_contents, ..
376                                        } => ElementMatchInfo {
377                                            element_key: None,
378                                            resolved_type: None,
379                                            process_contents: Some(*process_contents),
380                                        },
381                                    };
382                                    return Some(info);
383                                }
384                                return None;
385                            }
386                        }
387
388                        // No all-group particle matched — if all-group is satisfied,
389                        // try transitioning to the extension NFA
390                        if state.is_satisfied(model) {
391                            let initial = ActiveStates::from_nfa(extension_nfa);
392                            let mi = initial.find_match_info(
393                                extension_nfa,
394                                name,
395                                namespace,
396                                target_ns,
397                                subst_groups,
398                                xsd_version,
399                            );
400                            let match_info = ElementMatchInfo {
401                                element_key: mi.element_key,
402                                resolved_type: mi.resolved_type,
403                                process_contents: mi.process_contents,
404                            };
405                            let next = initial.advance_with_priority(
406                                extension_nfa,
407                                name,
408                                namespace,
409                                target_ns,
410                                subst_groups,
411                                xsd_version,
412                            );
413                            if !next.is_empty() {
414                                *phase = AllGroupExtPhase::Nfa(next);
415                                return Some(match_info);
416                            }
417                        }
418
419                        // Try open content wildcard as final fallback
420                        if let Some(oc) = &model.open_content {
421                            let allow = match oc.mode {
422                                AllGroupOpenContentMode::Interleave => true,
423                                AllGroupOpenContentMode::Suffix => state.is_satisfied(model),
424                                AllGroupOpenContentMode::None => false,
425                            };
426                            if allow
427                                && open_content_allows(
428                                    &oc.namespace_constraint,
429                                    &oc.not_qnames,
430                                    name,
431                                    namespace,
432                                    target_ns,
433                                )
434                            {
435                                return Some(ElementMatchInfo {
436                                    element_key: None,
437                                    resolved_type: None,
438                                    process_contents: Some(oc.process_contents),
439                                });
440                            }
441                        }
442                        None
443                    }
444                    AllGroupExtPhase::Nfa(active_states) => {
445                        // Standard NFA advancement in extension phase
446                        let mi = active_states.find_match_info(
447                            extension_nfa,
448                            name,
449                            namespace,
450                            target_ns,
451                            subst_groups,
452                            xsd_version,
453                        );
454                        let match_info = ElementMatchInfo {
455                            element_key: mi.element_key,
456                            resolved_type: mi.resolved_type,
457                            process_contents: mi.process_contents,
458                        };
459                        let next = active_states.clone().advance_with_priority(
460                            extension_nfa,
461                            name,
462                            namespace,
463                            target_ns,
464                            subst_groups,
465                            xsd_version,
466                        );
467                        if next.is_empty() {
468                            // Try open content wildcard fallback
469                            if let Some(oc) = &model.open_content {
470                                let allow = match oc.mode {
471                                    AllGroupOpenContentMode::Interleave => true,
472                                    AllGroupOpenContentMode::Suffix => {
473                                        active_states.contains_accept(extension_nfa)
474                                    }
475                                    AllGroupOpenContentMode::None => false,
476                                };
477                                if allow
478                                    && open_content_allows(
479                                        &oc.namespace_constraint,
480                                        &oc.not_qnames,
481                                        name,
482                                        namespace,
483                                        target_ns,
484                                    )
485                                {
486                                    return Some(ElementMatchInfo {
487                                        element_key: None,
488                                        resolved_type: None,
489                                        process_contents: Some(oc.process_contents),
490                                    });
491                                }
492                            }
493                            return None;
494                        }
495                        *active_states = next;
496                        Some(match_info)
497                    }
498                }
499            }
500            ContentValidatorState::Simple | ContentValidatorState::Empty => {
501                // Simple and Empty content models do not accept child elements
502                None
503            }
504        }
505    }
506
507    /// Check whether the content model is in a complete (accepting) state
508    ///
509    /// For NFA: any active state is the accept state.
510    /// For AllGroup: all required particles have been satisfied.
511    pub fn is_complete(&self) -> bool {
512        match self {
513            ContentValidatorState::Nfa {
514                nfa, active_states, ..
515            } => active_states.contains_accept(nfa),
516            ContentValidatorState::AllGroup { model, state, .. } => {
517                // If the outer particle is optional (minOccurs=0) and no children
518                // have been consumed, the entire group was skipped — trivially satisfied.
519                if model.outer_optional && !state.has_any_consumed() {
520                    return true;
521                }
522                state.is_satisfied(model)
523            }
524            #[cfg(feature = "xsd11")]
525            ContentValidatorState::AllGroupExtension {
526                model,
527                state,
528                extension_nfa,
529                phase,
530            } => {
531                // All-group must be satisfied (or skipped if outer-optional)
532                let all_satisfied = if model.outer_optional && !state.has_any_consumed() {
533                    true
534                } else {
535                    state.is_satisfied(model)
536                };
537                if !all_satisfied {
538                    return false;
539                }
540                match phase {
541                    AllGroupExtPhase::AllGroup => {
542                        // Still in all-group phase — extension NFA must accept empty
543                        let initial = ActiveStates::from_nfa(extension_nfa);
544                        initial.contains_accept(extension_nfa)
545                    }
546                    AllGroupExtPhase::Nfa(active_states) => {
547                        active_states.contains_accept(extension_nfa)
548                    }
549                }
550            }
551            ContentValidatorState::Simple | ContentValidatorState::Empty => true,
552        }
553    }
554
555    /// Non-mutating lookahead: would the given element be accepted?
556    ///
557    /// This does not change the state of the content model.
558    pub fn would_accept(
559        &self,
560        name: NameId,
561        namespace: Option<NameId>,
562        target_ns: Option<NameId>,
563        xsd_version: XsdVersion,
564        subst_groups: Option<&SubstitutionGroupMap>,
565    ) -> bool {
566        match self {
567            ContentValidatorState::Nfa {
568                nfa,
569                active_states,
570                open_content,
571            } => {
572                let next = match xsd_version {
573                    XsdVersion::V1_0 => active_states.clone().advance(
574                        nfa,
575                        name,
576                        namespace,
577                        target_ns,
578                        subst_groups,
579                        xsd_version,
580                    ),
581                    XsdVersion::V1_1 => active_states.clone().advance_with_priority(
582                        nfa,
583                        name,
584                        namespace,
585                        target_ns,
586                        subst_groups,
587                        xsd_version,
588                    ),
589                };
590                if !next.is_empty() {
591                    return true;
592                }
593                // Try open content wildcard fallback
594                if let Some(oc) = open_content {
595                    let allow = match oc.mode {
596                        TypesOpenContentMode::Interleave => true,
597                        TypesOpenContentMode::Suffix => active_states.contains_accept(nfa),
598                        TypesOpenContentMode::None => false,
599                    };
600                    if allow
601                        && open_content_allows(
602                            &oc.namespace_constraint,
603                            &oc.not_qnames,
604                            name,
605                            namespace,
606                            target_ns,
607                        )
608                    {
609                        return true;
610                    }
611                }
612                false
613            }
614            ContentValidatorState::AllGroup {
615                model,
616                state,
617                suffix_locked,
618            } => {
619                if !*suffix_locked {
620                    for (i, particle) in model.particles.iter().enumerate() {
621                        if !state.can_accept(model, i) {
622                            continue;
623                        }
624                        let result = term_matches_with_substitution(
625                            &particle.term,
626                            name,
627                            namespace,
628                            target_ns,
629                            subst_groups,
630                            xsd_version,
631                        );
632                        if result == TermMatchResult::Match {
633                            return true;
634                        }
635                    }
636                }
637                // Try open content wildcard fallback
638                if let Some(oc) = &model.open_content {
639                    let allow = match oc.mode {
640                        AllGroupOpenContentMode::Interleave => true,
641                        AllGroupOpenContentMode::Suffix => state.is_satisfied(model),
642                        AllGroupOpenContentMode::None => false,
643                    };
644                    if allow
645                        && open_content_allows(
646                            &oc.namespace_constraint,
647                            &oc.not_qnames,
648                            name,
649                            namespace,
650                            target_ns,
651                        )
652                    {
653                        return true;
654                    }
655                }
656                false
657            }
658            #[cfg(feature = "xsd11")]
659            ContentValidatorState::AllGroupExtension {
660                model,
661                state,
662                extension_nfa,
663                phase,
664            } => {
665                match phase {
666                    AllGroupExtPhase::AllGroup => {
667                        // Check all-group particles
668                        for (i, particle) in model.particles.iter().enumerate() {
669                            if !state.can_accept(model, i) {
670                                continue;
671                            }
672                            let result = term_matches_with_substitution(
673                                &particle.term,
674                                name,
675                                namespace,
676                                target_ns,
677                                subst_groups,
678                                xsd_version,
679                            );
680                            if result == TermMatchResult::Match {
681                                return true;
682                            }
683                        }
684                        // If all-group is satisfied, check extension NFA start
685                        if state.is_satisfied(model) {
686                            let initial = ActiveStates::from_nfa(extension_nfa);
687                            let next = initial.advance_with_priority(
688                                extension_nfa,
689                                name,
690                                namespace,
691                                target_ns,
692                                subst_groups,
693                                xsd_version,
694                            );
695                            if !next.is_empty() {
696                                return true;
697                            }
698                        }
699                        // Try open content wildcard fallback
700                        if let Some(oc) = &model.open_content {
701                            let allow = match oc.mode {
702                                AllGroupOpenContentMode::Interleave => true,
703                                AllGroupOpenContentMode::Suffix => state.is_satisfied(model),
704                                AllGroupOpenContentMode::None => false,
705                            };
706                            if allow
707                                && open_content_allows(
708                                    &oc.namespace_constraint,
709                                    &oc.not_qnames,
710                                    name,
711                                    namespace,
712                                    target_ns,
713                                )
714                            {
715                                return true;
716                            }
717                        }
718                        false
719                    }
720                    AllGroupExtPhase::Nfa(active_states) => {
721                        // Standard NFA lookahead
722                        let next = active_states.clone().advance_with_priority(
723                            extension_nfa,
724                            name,
725                            namespace,
726                            target_ns,
727                            subst_groups,
728                            xsd_version,
729                        );
730                        if !next.is_empty() {
731                            return true;
732                        }
733                        // Try open content wildcard fallback
734                        if let Some(oc) = &model.open_content {
735                            let allow = match oc.mode {
736                                AllGroupOpenContentMode::Interleave => true,
737                                AllGroupOpenContentMode::Suffix => {
738                                    active_states.contains_accept(extension_nfa)
739                                }
740                                AllGroupOpenContentMode::None => false,
741                            };
742                            if allow
743                                && open_content_allows(
744                                    &oc.namespace_constraint,
745                                    &oc.not_qnames,
746                                    name,
747                                    namespace,
748                                    target_ns,
749                                )
750                            {
751                                return true;
752                            }
753                        }
754                        false
755                    }
756                }
757            }
758            ContentValidatorState::Simple | ContentValidatorState::Empty => false,
759        }
760    }
761}
762
763/// Check if an open content wildcard allows the given element.
764/// Combines namespace matching with notQName exclusion checking.
765/// Open content is an XSD 1.1 feature, so V1_1 semantics always apply.
766fn open_content_allows(
767    ns_constraint: &NamespaceConstraint,
768    not_qnames: &[(Option<NameId>, NameId)],
769    name: NameId,
770    namespace: Option<NameId>,
771    target_ns: Option<NameId>,
772) -> bool {
773    ns_constraint.matches(namespace, target_ns, XsdVersion::V1_1)
774        && !not_qnames_exclude(not_qnames, namespace, name)
775}
776
777#[cfg(test)]
778mod tests {
779    use super::*;
780    use crate::compiler::{NfaState, NfaTerm};
781
782    /// Build a simple NFA that accepts a single element with given name_id
783    fn single_element_nfa(name: NameId, namespace: Option<NameId>) -> NfaTable {
784        // State 0: start (epsilon) -> State 1
785        // State 1: element term, consume -> State 2
786        // State 2: accept (epsilon)
787        let mut s0 = NfaState::epsilon(0, None);
788        s0.add_epsilon(1);
789
790        let mut s1 = NfaState::with_term(1, NfaTerm::element(name, namespace, None), None);
791        s1.add_consume(2);
792
793        let s2 = NfaState::epsilon(2, None);
794
795        NfaTable::new(vec![s0, s1, s2], 0, 2)
796    }
797
798    /// Build an NFA that accepts a sequence: elem_a then elem_b
799    fn sequence_nfa(
800        name_a: NameId,
801        ns_a: Option<NameId>,
802        name_b: NameId,
803        ns_b: Option<NameId>,
804    ) -> NfaTable {
805        // State 0: start (epsilon) -> State 1
806        // State 1: element A, consume -> State 2
807        // State 2: epsilon -> State 3
808        // State 3: element B, consume -> State 4
809        // State 4: accept (epsilon)
810        let mut s0 = NfaState::epsilon(0, None);
811        s0.add_epsilon(1);
812
813        let mut s1 = NfaState::with_term(1, NfaTerm::element(name_a, ns_a, None), None);
814        s1.add_consume(2);
815
816        let mut s2 = NfaState::epsilon(2, None);
817        s2.add_epsilon(3);
818
819        let mut s3 = NfaState::with_term(3, NfaTerm::element(name_b, ns_b, None), None);
820        s3.add_consume(4);
821
822        let s4 = NfaState::epsilon(4, None);
823
824        NfaTable::new(vec![s0, s1, s2, s3, s4], 0, 4)
825    }
826
827    #[test]
828    fn test_nfa_single_element_accepted() {
829        let name = NameId(1);
830        let nfa = single_element_nfa(name, None);
831        let mut state = ContentValidatorState::from_nfa(nfa);
832
833        assert!(
834            !state.is_complete(),
835            "should not be complete before any element"
836        );
837        assert!(state
838            .advance_element(name, None, None, XsdVersion::V1_0, None)
839            .is_some());
840        assert!(
841            state.is_complete(),
842            "should be complete after matching element"
843        );
844    }
845
846    #[test]
847    fn test_nfa_single_element_rejected() {
848        let name = NameId(1);
849        let wrong_name = NameId(2);
850        let nfa = single_element_nfa(name, None);
851        let mut state = ContentValidatorState::from_nfa(nfa);
852
853        assert!(state
854            .advance_element(wrong_name, None, None, XsdVersion::V1_0, None)
855            .is_none());
856        assert!(!state.is_complete());
857    }
858
859    #[test]
860    fn test_nfa_sequence() {
861        let a = NameId(10);
862        let b = NameId(20);
863        let nfa = sequence_nfa(a, None, b, None);
864        let mut state = ContentValidatorState::from_nfa(nfa);
865
866        assert!(!state.is_complete());
867        assert!(state
868            .advance_element(a, None, None, XsdVersion::V1_0, None)
869            .is_some());
870        assert!(!state.is_complete(), "only first element seen");
871        assert!(state
872            .advance_element(b, None, None, XsdVersion::V1_0, None)
873            .is_some());
874        assert!(state.is_complete(), "both elements matched");
875    }
876
877    #[test]
878    fn test_nfa_sequence_wrong_order() {
879        let a = NameId(10);
880        let b = NameId(20);
881        let nfa = sequence_nfa(a, None, b, None);
882        let mut state = ContentValidatorState::from_nfa(nfa);
883
884        // Try b first - should be rejected
885        assert!(state
886            .advance_element(b, None, None, XsdVersion::V1_0, None)
887            .is_none());
888    }
889
890    #[test]
891    fn test_nfa_would_accept() {
892        let name = NameId(1);
893        let wrong = NameId(2);
894        let nfa = single_element_nfa(name, None);
895        let state = ContentValidatorState::from_nfa(nfa);
896
897        assert!(state.would_accept(name, None, None, XsdVersion::V1_0, None));
898        assert!(!state.would_accept(wrong, None, None, XsdVersion::V1_0, None));
899    }
900
901    #[test]
902    fn test_all_group_any_order() {
903        use crate::compiler::{AllParticle, MaxOccurs};
904
905        let a = NameId(10);
906        let b = NameId(20);
907
908        let model = AllGroupModel::new(vec![
909            AllParticle::new(
910                NfaTerm::element(a, None, None),
911                1,
912                MaxOccurs::Bounded(1),
913                None,
914            ),
915            AllParticle::new(
916                NfaTerm::element(b, None, None),
917                1,
918                MaxOccurs::Bounded(1),
919                None,
920            ),
921        ]);
922
923        // Order: b, a (reversed) should still work
924        let mut state = ContentValidatorState::from_all_group(model);
925        assert!(!state.is_complete());
926        assert!(state
927            .advance_element(b, None, None, XsdVersion::V1_0, None)
928            .is_some());
929        assert!(
930            !state.is_complete(),
931            "only one of two required particles matched"
932        );
933        assert!(state
934            .advance_element(a, None, None, XsdVersion::V1_0, None)
935            .is_some());
936        assert!(state.is_complete(), "both particles satisfied");
937    }
938
939    #[test]
940    fn test_all_group_missing_required() {
941        use crate::compiler::{AllParticle, MaxOccurs};
942
943        let a = NameId(10);
944        let b = NameId(20);
945
946        let model = AllGroupModel::new(vec![
947            AllParticle::new(
948                NfaTerm::element(a, None, None),
949                1,
950                MaxOccurs::Bounded(1),
951                None,
952            ),
953            AllParticle::new(
954                NfaTerm::element(b, None, None),
955                1,
956                MaxOccurs::Bounded(1),
957                None,
958            ),
959        ]);
960
961        let mut state = ContentValidatorState::from_all_group(model);
962        assert!(state
963            .advance_element(a, None, None, XsdVersion::V1_0, None)
964            .is_some());
965        // Don't supply b
966        assert!(!state.is_complete(), "b is still required");
967    }
968
969    #[test]
970    fn test_simple_rejects_elements() {
971        let mut state = ContentValidatorState::Simple;
972        assert!(state
973            .advance_element(NameId(1), None, None, XsdVersion::V1_0, None)
974            .is_none());
975        assert!(state.is_complete());
976    }
977
978    #[test]
979    fn test_empty_rejects_elements() {
980        let mut state = ContentValidatorState::Empty;
981        assert!(state
982            .advance_element(NameId(1), None, None, XsdVersion::V1_0, None)
983            .is_none());
984        assert!(state.is_complete());
985    }
986
987    #[test]
988    fn test_from_matcher_nfa() {
989        let name = NameId(1);
990        let nfa = single_element_nfa(name, None);
991        let matcher = ContentModelMatcher::Nfa(nfa);
992        let mut state = ContentValidatorState::from_matcher(matcher);
993        assert!(state
994            .advance_element(name, None, None, XsdVersion::V1_0, None)
995            .is_some());
996        assert!(state.is_complete());
997    }
998
999    #[test]
1000    fn test_from_matcher_all_group() {
1001        use crate::compiler::{AllParticle, MaxOccurs};
1002
1003        let a = NameId(5);
1004        let model = AllGroupModel::new(vec![AllParticle::new(
1005            NfaTerm::element(a, None, None),
1006            0,
1007            MaxOccurs::Bounded(1),
1008            None,
1009        )]);
1010        let matcher = ContentModelMatcher::AllGroup(model);
1011        let state = ContentValidatorState::from_matcher(matcher);
1012        // Optional particle, so complete even without matching
1013        assert!(state.is_complete());
1014    }
1015
1016    // -- Open content tests --------------------------------------------------
1017
1018    use crate::compiler::OpenContentMode as AllGroupOCMode;
1019    use crate::compiler::{AllParticle, MaxOccurs, OpenContentWildcard};
1020
1021    fn all_group_with_open_content(
1022        mode: AllGroupOCMode,
1023        ns_constraint: NamespaceConstraint,
1024    ) -> AllGroupModel {
1025        let a = NameId(10);
1026        let mut model = AllGroupModel::new(vec![AllParticle::new(
1027            NfaTerm::element(a, None, None),
1028            1,
1029            MaxOccurs::Bounded(1),
1030            None,
1031        )]);
1032        model.open_content = Some(OpenContentWildcard {
1033            namespace_constraint: ns_constraint,
1034            process_contents: ProcessContents::Lax,
1035            mode,
1036            not_qnames: Vec::new(),
1037        });
1038        model
1039    }
1040
1041    #[test]
1042    fn test_all_group_open_content_interleave() {
1043        let model =
1044            all_group_with_open_content(AllGroupOCMode::Interleave, NamespaceConstraint::Any);
1045        let mut state = ContentValidatorState::from_all_group(model);
1046
1047        let extra = NameId(99);
1048        let a = NameId(10);
1049
1050        // Extra element accepted via open content before the declared particle
1051        let info = state.advance_element(extra, None, None, XsdVersion::V1_1, None);
1052        assert!(info.is_some(), "interleave should accept extra element");
1053        assert!(info.unwrap().process_contents.is_some());
1054
1055        // Declared particle still works
1056        assert!(state
1057            .advance_element(a, None, None, XsdVersion::V1_1, None)
1058            .is_some());
1059        assert!(state.is_complete());
1060
1061        // Extra element accepted after declared particle too
1062        let info2 = state.advance_element(extra, None, None, XsdVersion::V1_1, None);
1063        assert!(
1064            info2.is_some(),
1065            "interleave should accept extra element after satisfaction"
1066        );
1067    }
1068
1069    #[test]
1070    fn test_all_group_open_content_suffix() {
1071        let model = all_group_with_open_content(AllGroupOCMode::Suffix, NamespaceConstraint::Any);
1072        let mut state = ContentValidatorState::from_all_group(model);
1073
1074        let extra = NameId(99);
1075        let a = NameId(10);
1076
1077        // Extra element rejected before the required particle is satisfied
1078        assert!(
1079            state
1080                .advance_element(extra, None, None, XsdVersion::V1_1, None)
1081                .is_none(),
1082            "suffix should reject extra element before satisfaction"
1083        );
1084
1085        // Satisfy the declared particle
1086        assert!(state
1087            .advance_element(a, None, None, XsdVersion::V1_1, None)
1088            .is_some());
1089        assert!(state.is_complete());
1090
1091        // Now extra element should be accepted
1092        assert!(
1093            state
1094                .advance_element(extra, None, None, XsdVersion::V1_1, None)
1095                .is_some(),
1096            "suffix should accept extra element after satisfaction"
1097        );
1098    }
1099
1100    #[test]
1101    fn test_nfa_open_content_interleave() {
1102        let a = NameId(10);
1103        let nfa = single_element_nfa(a, None);
1104        let oc = OpenContentInfo {
1105            mode: TypesOpenContentMode::Interleave,
1106            namespace_constraint: NamespaceConstraint::Any,
1107            process_contents: ProcessContents::Lax,
1108            not_qnames: Vec::new(),
1109        };
1110        let initial = ActiveStates::from_nfa(&nfa);
1111        let mut state = ContentValidatorState::Nfa {
1112            nfa,
1113            active_states: initial,
1114            open_content: Some(oc),
1115        };
1116
1117        let extra = NameId(99);
1118
1119        // Extra element accepted via open content before the declared element
1120        let info = state.advance_element(extra, None, None, XsdVersion::V1_1, None);
1121        assert!(
1122            info.is_some(),
1123            "interleave should accept extra element before NFA match"
1124        );
1125
1126        // Declared element still works
1127        assert!(state
1128            .advance_element(a, None, None, XsdVersion::V1_1, None)
1129            .is_some());
1130        assert!(state.is_complete());
1131    }
1132
1133    #[test]
1134    fn test_nfa_open_content_suffix() {
1135        let a = NameId(10);
1136        let nfa = single_element_nfa(a, None);
1137        let oc = OpenContentInfo {
1138            mode: TypesOpenContentMode::Suffix,
1139            namespace_constraint: NamespaceConstraint::Any,
1140            process_contents: ProcessContents::Lax,
1141            not_qnames: Vec::new(),
1142        };
1143        let initial = ActiveStates::from_nfa(&nfa);
1144        let mut state = ContentValidatorState::Nfa {
1145            nfa,
1146            active_states: initial,
1147            open_content: Some(oc),
1148        };
1149
1150        let extra = NameId(99);
1151        let a = NameId(10);
1152
1153        // Extra element rejected before accept state
1154        assert!(
1155            state
1156                .advance_element(extra, None, None, XsdVersion::V1_1, None)
1157                .is_none(),
1158            "suffix should reject extra element before accept state"
1159        );
1160
1161        // Match declared element to reach accept state
1162        assert!(state
1163            .advance_element(a, None, None, XsdVersion::V1_1, None)
1164            .is_some());
1165        assert!(state.is_complete());
1166
1167        // Now extra element accepted
1168        assert!(
1169            state
1170                .advance_element(extra, None, None, XsdVersion::V1_1, None)
1171                .is_some(),
1172            "suffix should accept extra element after accept state"
1173        );
1174    }
1175
1176    #[test]
1177    fn test_open_content_namespace_constraint() {
1178        let target_ns = Some(NameId(100));
1179        let other_ns = Some(NameId(200));
1180
1181        let model = all_group_with_open_content(
1182            AllGroupOCMode::Interleave,
1183            NamespaceConstraint::Other, // Only accept elements from other namespaces
1184        );
1185        let mut state = ContentValidatorState::from_all_group(model);
1186
1187        let extra = NameId(99);
1188
1189        // Element from target namespace should be rejected by open content
1190        assert!(
1191            state
1192                .advance_element(extra, target_ns, target_ns, XsdVersion::V1_1, None)
1193                .is_none(),
1194            "open content with ##other should reject target namespace"
1195        );
1196
1197        // Element from other namespace should be accepted
1198        assert!(
1199            state
1200                .advance_element(extra, other_ns, target_ns, XsdVersion::V1_1, None)
1201                .is_some(),
1202            "open content with ##other should accept other namespace"
1203        );
1204    }
1205
1206    #[test]
1207    fn test_would_accept_with_open_content() {
1208        let model =
1209            all_group_with_open_content(AllGroupOCMode::Interleave, NamespaceConstraint::Any);
1210        let state = ContentValidatorState::from_all_group(model);
1211
1212        let extra = NameId(99);
1213        let a = NameId(10);
1214
1215        // Both declared and extra elements should be accepted in lookahead
1216        assert!(state.would_accept(a, None, None, XsdVersion::V1_1, None));
1217        assert!(state.would_accept(extra, None, None, XsdVersion::V1_1, None));
1218
1219        // NFA version
1220        let nfa = single_element_nfa(a, None);
1221        let oc = OpenContentInfo {
1222            mode: TypesOpenContentMode::Interleave,
1223            namespace_constraint: NamespaceConstraint::Any,
1224            process_contents: ProcessContents::Lax,
1225            not_qnames: Vec::new(),
1226        };
1227        let initial = ActiveStates::from_nfa(&nfa);
1228        let state = ContentValidatorState::Nfa {
1229            nfa,
1230            active_states: initial,
1231            open_content: Some(oc),
1232        };
1233        assert!(state.would_accept(a, None, None, XsdVersion::V1_1, None));
1234        assert!(state.would_accept(extra, None, None, XsdVersion::V1_1, None));
1235    }
1236
1237    // -- AllGroupExtension tests (XSD 1.1) ------------------------------------
1238
1239    #[cfg(feature = "xsd11")]
1240    fn make_all_group_extension_state(
1241        all_particles: Vec<AllParticle>,
1242        ext_nfa: NfaTable,
1243    ) -> ContentValidatorState {
1244        let model = AllGroupModel::new(all_particles);
1245        let matcher = ContentModelMatcher::AllGroupExtension {
1246            base_model: model,
1247            extension_nfa: ext_nfa,
1248        };
1249        ContentValidatorState::from_matcher(matcher)
1250    }
1251
1252    /// all(A, B) + seq(C): accepts A,B,C and B,A,C; rejects C,A,B and A,C,B
1253    #[cfg(feature = "xsd11")]
1254    #[test]
1255    fn test_all_group_extension_basic_composite() {
1256        let a = NameId(10);
1257        let b = NameId(20);
1258        let c = NameId(30);
1259
1260        let particles = vec![
1261            AllParticle::new(
1262                NfaTerm::element(a, None, None),
1263                1,
1264                MaxOccurs::Bounded(1),
1265                None,
1266            ),
1267            AllParticle::new(
1268                NfaTerm::element(b, None, None),
1269                1,
1270                MaxOccurs::Bounded(1),
1271                None,
1272            ),
1273        ];
1274        let ext_nfa = single_element_nfa(c, None);
1275
1276        // A, B, C → accepted
1277        let mut state = make_all_group_extension_state(particles.clone(), ext_nfa.clone());
1278        assert!(state
1279            .advance_element(a, None, None, XsdVersion::V1_1, None)
1280            .is_some());
1281        assert!(state
1282            .advance_element(b, None, None, XsdVersion::V1_1, None)
1283            .is_some());
1284        assert!(state
1285            .advance_element(c, None, None, XsdVersion::V1_1, None)
1286            .is_some());
1287        assert!(state.is_complete());
1288
1289        // B, A, C → accepted (reversed all-group order)
1290        let mut state = make_all_group_extension_state(particles.clone(), ext_nfa.clone());
1291        assert!(state
1292            .advance_element(b, None, None, XsdVersion::V1_1, None)
1293            .is_some());
1294        assert!(state
1295            .advance_element(a, None, None, XsdVersion::V1_1, None)
1296            .is_some());
1297        assert!(state
1298            .advance_element(c, None, None, XsdVersion::V1_1, None)
1299            .is_some());
1300        assert!(state.is_complete());
1301
1302        // C, A, B → rejected (C before all-group is satisfied)
1303        let mut state = make_all_group_extension_state(particles.clone(), ext_nfa.clone());
1304        assert!(state
1305            .advance_element(c, None, None, XsdVersion::V1_1, None)
1306            .is_none());
1307
1308        // A, C, B → rejected (C before B satisfies all-group)
1309        let mut state = make_all_group_extension_state(particles, ext_nfa);
1310        assert!(state
1311            .advance_element(a, None, None, XsdVersion::V1_1, None)
1312            .is_some());
1313        assert!(state
1314            .advance_element(c, None, None, XsdVersion::V1_1, None)
1315            .is_none());
1316    }
1317
1318    /// all(A?, B?) + seq(C): accepts C alone (all-group satisfied empty)
1319    #[cfg(feature = "xsd11")]
1320    #[test]
1321    fn test_all_group_extension_optional_all_group() {
1322        let a = NameId(10);
1323        let b = NameId(20);
1324        let c = NameId(30);
1325
1326        let particles = vec![
1327            AllParticle::new(
1328                NfaTerm::element(a, None, None),
1329                0,
1330                MaxOccurs::Bounded(1),
1331                None,
1332            ),
1333            AllParticle::new(
1334                NfaTerm::element(b, None, None),
1335                0,
1336                MaxOccurs::Bounded(1),
1337                None,
1338            ),
1339        ];
1340        let ext_nfa = single_element_nfa(c, None);
1341
1342        // C alone → accepted (all-group is satisfied with zero occurrences)
1343        let mut state = make_all_group_extension_state(particles, ext_nfa);
1344        assert!(state
1345            .advance_element(c, None, None, XsdVersion::V1_1, None)
1346            .is_some());
1347        assert!(state.is_complete());
1348    }
1349
1350    /// is_complete checks: after A,B,C → complete; after A,B → not complete
1351    #[cfg(feature = "xsd11")]
1352    #[test]
1353    fn test_all_group_extension_is_complete() {
1354        let a = NameId(10);
1355        let b = NameId(20);
1356        let c = NameId(30);
1357
1358        let particles = vec![
1359            AllParticle::new(
1360                NfaTerm::element(a, None, None),
1361                1,
1362                MaxOccurs::Bounded(1),
1363                None,
1364            ),
1365            AllParticle::new(
1366                NfaTerm::element(b, None, None),
1367                1,
1368                MaxOccurs::Bounded(1),
1369                None,
1370            ),
1371        ];
1372        let ext_nfa = single_element_nfa(c, None);
1373
1374        let mut state = make_all_group_extension_state(particles, ext_nfa);
1375        assert!(!state.is_complete(), "not complete initially");
1376
1377        assert!(state
1378            .advance_element(a, None, None, XsdVersion::V1_1, None)
1379            .is_some());
1380        assert!(!state.is_complete(), "not complete after A only");
1381
1382        assert!(state
1383            .advance_element(b, None, None, XsdVersion::V1_1, None)
1384            .is_some());
1385        assert!(
1386            !state.is_complete(),
1387            "not complete after A,B — extension C still required"
1388        );
1389
1390        assert!(state
1391            .advance_element(c, None, None, XsdVersion::V1_1, None)
1392            .is_some());
1393        assert!(state.is_complete(), "complete after A,B,C");
1394    }
1395
1396    /// would_accept lookahead: initially A/B accepted, C not; after A,B only C accepted
1397    #[cfg(feature = "xsd11")]
1398    #[test]
1399    fn test_all_group_extension_would_accept() {
1400        let a = NameId(10);
1401        let b = NameId(20);
1402        let c = NameId(30);
1403
1404        let particles = vec![
1405            AllParticle::new(
1406                NfaTerm::element(a, None, None),
1407                1,
1408                MaxOccurs::Bounded(1),
1409                None,
1410            ),
1411            AllParticle::new(
1412                NfaTerm::element(b, None, None),
1413                1,
1414                MaxOccurs::Bounded(1),
1415                None,
1416            ),
1417        ];
1418        let ext_nfa = single_element_nfa(c, None);
1419
1420        let mut state = make_all_group_extension_state(particles, ext_nfa);
1421
1422        // Initially: A and B accepted, C not (all-group not yet satisfied)
1423        assert!(state.would_accept(a, None, None, XsdVersion::V1_1, None));
1424        assert!(state.would_accept(b, None, None, XsdVersion::V1_1, None));
1425        assert!(!state.would_accept(c, None, None, XsdVersion::V1_1, None));
1426
1427        // After A,B: only C is accepted
1428        state.advance_element(a, None, None, XsdVersion::V1_1, None);
1429        state.advance_element(b, None, None, XsdVersion::V1_1, None);
1430        assert!(!state.would_accept(a, None, None, XsdVersion::V1_1, None));
1431        assert!(!state.would_accept(b, None, None, XsdVersion::V1_1, None));
1432        assert!(state.would_accept(c, None, None, XsdVersion::V1_1, None));
1433    }
1434
1435    // -- Not constraint and notQName tests -----------------------------------
1436
1437    #[test]
1438    fn test_open_content_not_namespace_constraint() {
1439        // Open content with Not([ns1]) should reject ns1 but accept others
1440        let ns1 = Some(NameId(100));
1441        let ns2 = Some(NameId(200));
1442        let a = NameId(10);
1443        let extra = NameId(99);
1444
1445        let mut model = AllGroupModel::new(vec![AllParticle::new(
1446            NfaTerm::element(a, None, None),
1447            1,
1448            MaxOccurs::Bounded(1),
1449            None,
1450        )]);
1451        model.open_content = Some(OpenContentWildcard {
1452            namespace_constraint: NamespaceConstraint::Not(vec![ns1]),
1453            process_contents: ProcessContents::Lax,
1454            mode: AllGroupOCMode::Interleave,
1455            not_qnames: Vec::new(),
1456        });
1457        let mut state = ContentValidatorState::from_all_group(model);
1458
1459        // Element from excluded namespace rejected
1460        assert!(
1461            state
1462                .advance_element(extra, ns1, None, XsdVersion::V1_1, None)
1463                .is_none(),
1464            "Not([ns1]) should reject elements from ns1"
1465        );
1466
1467        // Element from other namespace accepted
1468        assert!(
1469            state
1470                .advance_element(extra, ns2, None, XsdVersion::V1_1, None)
1471                .is_some(),
1472            "Not([ns1]) should accept elements from ns2"
1473        );
1474    }
1475
1476    #[test]
1477    fn test_open_content_not_qnames_exclusion() {
1478        // Open content with notQName excluding specific element
1479        let a = NameId(10);
1480        let excluded = NameId(50);
1481        let allowed = NameId(60);
1482
1483        let mut model = AllGroupModel::new(vec![AllParticle::new(
1484            NfaTerm::element(a, None, None),
1485            1,
1486            MaxOccurs::Bounded(1),
1487            None,
1488        )]);
1489        model.open_content = Some(OpenContentWildcard {
1490            namespace_constraint: NamespaceConstraint::Any,
1491            process_contents: ProcessContents::Lax,
1492            mode: AllGroupOCMode::Interleave,
1493            not_qnames: vec![(None, excluded)], // exclude (absent ns, excluded)
1494        });
1495        let mut state = ContentValidatorState::from_all_group(model);
1496
1497        // Excluded element rejected even though namespace matches
1498        assert!(
1499            state
1500                .advance_element(excluded, None, None, XsdVersion::V1_1, None)
1501                .is_none(),
1502            "notQName should reject excluded element"
1503        );
1504
1505        // Non-excluded element accepted
1506        assert!(
1507            state
1508                .advance_element(allowed, None, None, XsdVersion::V1_1, None)
1509                .is_some(),
1510            "notQName should accept non-excluded element"
1511        );
1512    }
1513
1514    #[test]
1515    fn test_nfa_open_content_not_qnames_exclusion() {
1516        // Same test but for NFA path
1517        let a = NameId(10);
1518        let excluded = NameId(50);
1519        let allowed = NameId(60);
1520        let nfa = single_element_nfa(a, None);
1521        let oc = OpenContentInfo {
1522            mode: TypesOpenContentMode::Interleave,
1523            namespace_constraint: NamespaceConstraint::Any,
1524            process_contents: ProcessContents::Lax,
1525            not_qnames: vec![(None, excluded)],
1526        };
1527        let initial = ActiveStates::from_nfa(&nfa);
1528        let mut state = ContentValidatorState::Nfa {
1529            nfa,
1530            active_states: initial,
1531            open_content: Some(oc),
1532        };
1533
1534        // Excluded element rejected
1535        assert!(
1536            state
1537                .advance_element(excluded, None, None, XsdVersion::V1_1, None)
1538                .is_none(),
1539            "NFA open content notQName should reject excluded element"
1540        );
1541
1542        // Non-excluded element accepted
1543        assert!(
1544            state
1545                .advance_element(allowed, None, None, XsdVersion::V1_1, None)
1546                .is_some(),
1547            "NFA open content notQName should accept non-excluded element"
1548        );
1549    }
1550
1551    #[test]
1552    fn test_would_accept_respects_not_qnames() {
1553        let a = NameId(10);
1554        let excluded = NameId(50);
1555        let allowed = NameId(60);
1556
1557        let mut model = AllGroupModel::new(vec![AllParticle::new(
1558            NfaTerm::element(a, None, None),
1559            1,
1560            MaxOccurs::Bounded(1),
1561            None,
1562        )]);
1563        model.open_content = Some(OpenContentWildcard {
1564            namespace_constraint: NamespaceConstraint::Any,
1565            process_contents: ProcessContents::Lax,
1566            mode: AllGroupOCMode::Interleave,
1567            not_qnames: vec![(None, excluded)],
1568        });
1569        let state = ContentValidatorState::from_all_group(model);
1570
1571        assert!(!state.would_accept(excluded, None, None, XsdVersion::V1_1, None));
1572        assert!(state.would_accept(allowed, None, None, XsdVersion::V1_1, None));
1573    }
1574}