Skip to main content

xsd_schema/parser/
structure.rs

1//! Structural validation rules for XSD elements
2//!
3//! This module provides validation functions for XSD structural constraints
4//! that cannot be expressed in the frame's `allows()` method. These include:
5//!
6//! - **Mutually exclusive attributes**: e.g., `name` XOR `ref` on elements
7//! - **Dependent attributes**: e.g., `keyref` requires `refer`
8//! - **Prohibited combinations**: e.g., top-level element cannot have `minOccurs`
9//! - **XSD version gates**: e.g., `xs:assert` requires XSD 1.1
10//!
11//! # XSD Structural Constraints
12//!
13//! Per W3C XSD 1.0 specification:
14//!
15//! | Element | Constraint |
16//! |---------|------------|
17//! | element (top) | Must have `name`, must not have `ref`/`minOccurs`/`maxOccurs` |
18//! | element (local) | Must have exactly one of `name` or `ref` |
19//! | attribute (top) | Must have `name`, must not have `ref`/`use` |
20//! | attribute (local) | Must have exactly one of `name` or `ref` |
21//! | simpleType | `name` required at top level, prohibited in local context |
22//! | complexType | `name` required at top level, prohibited in local context |
23//! | restriction | Must have `base` XOR inline type |
24//! | extension | Must have `base` attribute |
25//! | key/unique | Must have `name` attribute |
26//! | keyref | Must have `name` and `refer` attributes |
27//! | list | Must have `itemType` XOR inline simpleType |
28//! | union | Must have `memberTypes` XOR inline simpleTypes |
29
30use crate::error::{SchemaError, SchemaResult};
31use crate::namespace::{is_ncname, NameTable};
32use crate::parser::attrs::AttributeMap;
33use crate::parser::location::SourceRef;
34use crate::schema::XsdVersion;
35use crate::types::facets::{normalize_whitespace, WhitespaceMode};
36
37/// Apply XSD whitespace=collapse to an attribute value before NCName/QName validation.
38/// XSD attribute types like xs:NCName have whiteSpace=collapse semantics — leading,
39/// trailing, and runs of internal whitespace are normalized away before the value
40/// is interpreted lexically.
41fn collapsed(value: &str) -> String {
42    normalize_whitespace(value, WhitespaceMode::Collapse)
43}
44
45/// Validation context for structural checks
46#[derive(Debug, Clone)]
47pub struct ValidationContext {
48    /// XSD version mode (1.0 or 1.1)
49    pub xsd_version: XsdVersion,
50    /// Whether this is a top-level (global) declaration
51    pub is_top_level: bool,
52    /// Whether this declaration has a `<complexType>` lexical ancestor.
53    /// Per src-element §3.3.3 / src-attribute §3.2.3, a local declaration
54    /// may carry `targetNamespace` only when it has a complexType ancestor.
55    pub inside_complex_type: bool,
56    /// Source reference for error reporting
57    pub source: Option<SourceRef>,
58}
59
60impl Default for ValidationContext {
61    fn default() -> Self {
62        Self {
63            xsd_version: XsdVersion::V1_0,
64            is_top_level: false,
65            inside_complex_type: false,
66            source: None,
67        }
68    }
69}
70
71impl ValidationContext {
72    /// Create a new validation context
73    pub fn new(xsd_version: XsdVersion, is_top_level: bool) -> Self {
74        Self {
75            xsd_version,
76            is_top_level,
77            inside_complex_type: false,
78            source: None,
79        }
80    }
81
82    /// Create a context with source reference
83    pub fn with_source(mut self, source: Option<SourceRef>) -> Self {
84        self.source = source;
85        self
86    }
87}
88
89// ============================================================================
90// Element Declaration Validation
91// ============================================================================
92
93/// Validate element declaration structural constraints
94///
95/// Top-level elements:
96/// - Must have `name` attribute
97/// - Must NOT have `ref`, `minOccurs`, `maxOccurs`, or `form` attributes
98///
99/// Local elements:
100/// - Must have exactly one of `name` OR `ref`
101/// - If `ref` is present, type/default/fixed/nillable/block/final are prohibited
102pub fn validate_element_structure(
103    attrs: &AttributeMap,
104    name_table: &NameTable,
105    ctx: &ValidationContext,
106) -> SchemaResult<()> {
107    let has_name = attrs.get_value_by_name(name_table, "name").is_some();
108    let has_ref = attrs.get_value_by_name(name_table, "ref").is_some();
109
110    // src-element.1: the name must be a valid NCName.
111    if let Some(name_val) = attrs.get_value_by_name(name_table, "name") {
112        if !is_ncname(&collapsed(name_val)) {
113            return Err(SchemaError::structural(
114                "src-element",
115                format!("Element 'name' value '{}' is not a valid NCName", name_val),
116                None,
117            ));
118        }
119    }
120
121    if ctx.is_top_level {
122        // Top-level element validation
123        if !has_name {
124            return Err(SchemaError::structural(
125                "src-element",
126                "Top-level element declaration must have 'name' attribute",
127                None,
128            ));
129        }
130
131        if has_ref {
132            return Err(SchemaError::structural(
133                "src-element",
134                "Top-level element declaration cannot have 'ref' attribute",
135                None,
136            ));
137        }
138
139        // Prohibited attributes for top-level. `targetNamespace` is only
140        // allowed on local declarations inside a `<complexType>` ancestor
141        // (src-element §3.3.3 clause 4).
142        for prohibited in &["minOccurs", "maxOccurs", "form", "targetNamespace"] {
143            if attrs.get_value_by_name(name_table, prohibited).is_some() {
144                return Err(SchemaError::structural(
145                    "src-element",
146                    format!(
147                        "Top-level element declaration cannot have '{}' attribute",
148                        prohibited
149                    ),
150                    None,
151                ));
152            }
153        }
154    } else {
155        // Local element validation
156        if has_name && has_ref {
157            return Err(SchemaError::structural(
158                "src-element",
159                "Local element cannot have both 'name' and 'ref' attributes",
160                None,
161            ));
162        }
163
164        if !has_name && !has_ref {
165            return Err(SchemaError::structural(
166                "src-element",
167                "Local element must have either 'name' or 'ref' attribute",
168                None,
169            ));
170        }
171
172        // If ref is present, certain attributes are prohibited.
173        // `targetNamespace` is forbidden on element refs because the
174        // referenced declaration already determines the namespace.
175        if has_ref {
176            let ref_prohibited = [
177                "type",
178                "default",
179                "fixed",
180                "nillable",
181                "block",
182                "final",
183                "form",
184                "targetNamespace",
185            ];
186            for prohibited in &ref_prohibited {
187                if attrs.get_value_by_name(name_table, prohibited).is_some() {
188                    return Err(SchemaError::structural(
189                        "src-element",
190                        format!("Element reference cannot have '{}' attribute", prohibited),
191                        None,
192                    ));
193                }
194            }
195        }
196
197        // src-element clause 3: `final`, `abstract`, `substitutionGroup` are
198        // restricted to global declarations.
199        for prohibited in &["final", "abstract", "substitutionGroup"] {
200            if attrs.get_value_by_name(name_table, prohibited).is_some() {
201                return Err(SchemaError::structural(
202                    "src-element",
203                    format!(
204                        "Local element declaration cannot have '{}' attribute",
205                        prohibited
206                    ),
207                    None,
208                ));
209            }
210        }
211
212        // src-element §3.3.3 clauses 3.2.2 / 4: `targetNamespace` may only
213        // appear on a local element when (a) `form` is absent and (b) there
214        // is a `<complexType>` lexical ancestor.
215        let has_target_ns = attrs
216            .get_value_by_name(name_table, "targetNamespace")
217            .is_some();
218        if has_target_ns {
219            let has_form = attrs.get_value_by_name(name_table, "form").is_some();
220            if has_form {
221                return Err(SchemaError::structural(
222                    "src-element",
223                    "Local element with 'targetNamespace' cannot also have 'form' \
224                     (src-element §3.3.3 clause 3.2.2)",
225                    None,
226                ));
227            }
228            if !ctx.inside_complex_type {
229                return Err(SchemaError::structural(
230                    "src-element",
231                    "Local element with 'targetNamespace' must have a <complexType> \
232                     lexical ancestor (src-element §3.3.3 clause 4)",
233                    None,
234                ));
235            }
236        }
237    }
238
239    // Validate default XOR fixed
240    let has_default = attrs.get_value_by_name(name_table, "default").is_some();
241    let has_fixed = attrs.get_value_by_name(name_table, "fixed").is_some();
242    if has_default && has_fixed {
243        return Err(SchemaError::structural(
244            "cos-valid-default",
245            "Element cannot have both 'default' and 'fixed' attributes",
246            None,
247        ));
248    }
249
250    // §3.3.2 element XML representation: `final` allows only `extension|restriction|#all`.
251    // (Substitution is permitted in `block`, but NOT `final`.)
252    if let Some(final_val) = attrs.get_value_by_name(name_table, "final") {
253        validate_derivation_set_tokens(
254            final_val,
255            &["extension", "restriction"],
256            "final",
257            "element",
258        )?;
259    }
260    // §3.3.2 element/@block allows `extension|restriction|substitution|#all`.
261    if let Some(block_val) = attrs.get_value_by_name(name_table, "block") {
262        validate_derivation_set_tokens(
263            block_val,
264            &["extension", "restriction", "substitution"],
265            "block",
266            "element",
267        )?;
268    }
269
270    Ok(())
271}
272
273/// Validate that a `block`/`final`-style attribute value contains only the
274/// derivation tokens permitted in the given context (plus `#all`).
275fn validate_derivation_set_tokens(
276    value: &str,
277    allowed: &[&str],
278    attr: &str,
279    elem: &str,
280) -> SchemaResult<()> {
281    let trimmed = value.trim();
282    if trimmed == "#all" {
283        return Ok(());
284    }
285    for token in trimmed.split_whitespace() {
286        if !allowed.contains(&token) {
287            return Err(SchemaError::structural(
288                "sch-props-correct",
289                format!(
290                    "'{}' on '{}' does not allow derivation method '{}'",
291                    attr, elem, token
292                ),
293                None,
294            ));
295        }
296    }
297    Ok(())
298}
299
300// ============================================================================
301// Attribute Declaration Validation
302// ============================================================================
303
304/// Validate attribute declaration structural constraints
305///
306/// Top-level attributes:
307/// - Must have `name` attribute
308/// - Must NOT have `ref`, `use`, or `form` attributes
309///
310/// Local attributes:
311/// - Must have exactly one of `name` OR `ref`
312/// - If `ref` is present, type/form are prohibited (src-attribute.3.2)
313/// - `default` and `fixed` ARE allowed on refs (they set the attribute use's value constraint)
314pub fn validate_attribute_structure(
315    attrs: &AttributeMap,
316    name_table: &NameTable,
317    ctx: &ValidationContext,
318) -> SchemaResult<()> {
319    let has_name = attrs.get_value_by_name(name_table, "name").is_some();
320    let has_ref = attrs.get_value_by_name(name_table, "ref").is_some();
321
322    // src-attribute.1 / src-element.1: the name must be a valid NCName.
323    if let Some(name_val) = attrs.get_value_by_name(name_table, "name") {
324        let collapsed_name = collapsed(name_val);
325        if !is_ncname(&collapsed_name) {
326            return Err(SchemaError::structural(
327                "src-attribute",
328                format!(
329                    "Attribute 'name' value '{}' is not a valid NCName",
330                    name_val
331                ),
332                None,
333            ));
334        }
335        // no-xmlns (§3.2.6.2): attribute declarations must not use the local
336        // name "xmlns".
337        if collapsed_name == "xmlns" {
338            return Err(SchemaError::structural(
339                "no-xmlns",
340                "Attribute declaration name must not be 'xmlns'",
341                None,
342            ));
343        }
344    }
345
346    if ctx.is_top_level {
347        // Top-level attribute validation
348        if !has_name {
349            return Err(SchemaError::structural(
350                "src-attribute",
351                "Top-level attribute declaration must have 'name' attribute",
352                None,
353            ));
354        }
355
356        if has_ref {
357            return Err(SchemaError::structural(
358                "src-attribute",
359                "Top-level attribute declaration cannot have 'ref' attribute",
360                None,
361            ));
362        }
363
364        // Prohibited attributes for top-level. `targetNamespace` is only
365        // allowed on local declarations inside a `<complexType>` ancestor
366        // (src-attribute §3.2.3 clause 6).
367        for prohibited in &["use", "form", "targetNamespace"] {
368            if attrs.get_value_by_name(name_table, prohibited).is_some() {
369                return Err(SchemaError::structural(
370                    "src-attribute",
371                    format!(
372                        "Top-level attribute declaration cannot have '{}' attribute",
373                        prohibited
374                    ),
375                    None,
376                ));
377            }
378        }
379    } else {
380        // Local attribute validation
381        if has_name && has_ref {
382            return Err(SchemaError::structural(
383                "src-attribute",
384                "Local attribute cannot have both 'name' and 'ref' attributes",
385                None,
386            ));
387        }
388
389        if !has_name && !has_ref {
390            return Err(SchemaError::structural(
391                "src-attribute",
392                "Local attribute must have either 'name' or 'ref' attribute",
393                None,
394            ));
395        }
396
397        // src-attribute.3.2: If ref is present, <simpleType>, form and type must be absent.
398        // Note: default and fixed ARE allowed on attribute references — they set the
399        // attribute use's value constraint, overriding the referenced declaration's.
400        // `targetNamespace` is also forbidden on attribute refs because the referenced
401        // declaration determines the namespace.
402        if has_ref {
403            for prohibited in &["type", "form", "targetNamespace"] {
404                if attrs.get_value_by_name(name_table, prohibited).is_some() {
405                    return Err(SchemaError::structural(
406                        "src-attribute",
407                        format!("Attribute reference cannot have '{}' attribute", prohibited),
408                        None,
409                    ));
410                }
411            }
412        }
413
414        // src-attribute §3.2.3 clauses 6.2 / 6: `targetNamespace` may only
415        // appear on a local attribute when (a) `form` is absent and (b)
416        // there is a `<complexType>` lexical ancestor.
417        let has_target_ns = attrs
418            .get_value_by_name(name_table, "targetNamespace")
419            .is_some();
420        if has_target_ns {
421            let has_form = attrs.get_value_by_name(name_table, "form").is_some();
422            if has_form {
423                return Err(SchemaError::structural(
424                    "src-attribute",
425                    "Local attribute with 'targetNamespace' cannot also have 'form' \
426                     (src-attribute §3.2.3 clause 6.2)",
427                    None,
428                ));
429            }
430            if !ctx.inside_complex_type {
431                return Err(SchemaError::structural(
432                    "src-attribute",
433                    "Local attribute with 'targetNamespace' must have a <complexType> \
434                     lexical ancestor (src-attribute §3.2.3 clause 6)",
435                    None,
436                ));
437            }
438        }
439    }
440
441    // Validate default XOR fixed
442    let has_default = attrs.get_value_by_name(name_table, "default").is_some();
443    let has_fixed = attrs.get_value_by_name(name_table, "fixed").is_some();
444    if has_default && has_fixed {
445        return Err(SchemaError::structural(
446            "cos-valid-default",
447            "Attribute cannot have both 'default' and 'fixed' attributes",
448            None,
449        ));
450    }
451
452    // src-attribute §3.2.3 clause 2: If default and use are both present, use must be "optional".
453    if has_default {
454        if let Some(use_val) = attrs.get_value_by_name(name_table, "use") {
455            if use_val != "optional" {
456                return Err(SchemaError::structural(
457                    "src-attribute",
458                    format!(
459                        "Attribute with 'default' must have use='optional' (got '{}')",
460                        use_val
461                    ),
462                    None,
463                ));
464            }
465        }
466    }
467
468    // Validate use="prohibited" conflicts
469    if let Some(use_val) = attrs.get_value_by_name(name_table, "use") {
470        if use_val == "prohibited" {
471            // src-attribute §3.2.3 clause 5: use="prohibited" + fixed is only an error in XSD 1.1.
472            // In XSD 1.0 the combination is syntactically odd but not explicitly forbidden.
473            if has_fixed && ctx.xsd_version == XsdVersion::V1_1 {
474                return Err(SchemaError::structural(
475                    "src-attribute",
476                    "Prohibited attribute cannot have 'fixed' attribute",
477                    None,
478                ));
479            }
480        }
481    }
482
483    Ok(())
484}
485
486// ============================================================================
487// Type Definition Validation
488// ============================================================================
489
490/// Validate simple type definition structure
491///
492/// - Top-level: `name` required
493/// - Local (inline): `name` prohibited
494/// - Must have exactly one of: restriction, list, or union child
495pub fn validate_simple_type_structure(
496    attrs: &AttributeMap,
497    name_table: &NameTable,
498    ctx: &ValidationContext,
499) -> SchemaResult<()> {
500    let has_name = attrs.get_value_by_name(name_table, "name").is_some();
501
502    if ctx.is_top_level && !has_name {
503        return Err(SchemaError::structural(
504            "src-simple-type",
505            "Top-level simpleType must have 'name' attribute",
506            None,
507        ));
508    }
509
510    if !ctx.is_top_level && has_name {
511        return Err(SchemaError::structural(
512            "src-simple-type",
513            "Inline simpleType cannot have 'name' attribute",
514            None,
515        ));
516    }
517
518    if let Some(name_val) = attrs.get_value_by_name(name_table, "name") {
519        if !is_ncname(&collapsed(name_val)) {
520            return Err(SchemaError::structural(
521                "src-simple-type",
522                format!(
523                    "simpleType 'name' value '{}' is not a valid NCName",
524                    name_val
525                ),
526                None,
527            ));
528        }
529    }
530
531    // §3.14.2 simpleType/@final allows `restriction|list|union|extension|#all`.
532    if let Some(final_val) = attrs.get_value_by_name(name_table, "final") {
533        validate_derivation_set_tokens(
534            final_val,
535            &["restriction", "list", "union", "extension"],
536            "final",
537            "simpleType",
538        )?;
539    }
540
541    Ok(())
542}
543
544/// Validate complex type definition structure
545///
546/// - Top-level: `name` required
547/// - Local (inline): `name` prohibited
548pub fn validate_complex_type_structure(
549    attrs: &AttributeMap,
550    name_table: &NameTable,
551    ctx: &ValidationContext,
552) -> SchemaResult<()> {
553    let has_name = attrs.get_value_by_name(name_table, "name").is_some();
554
555    if ctx.is_top_level && !has_name {
556        return Err(SchemaError::structural(
557            "src-ct",
558            "Top-level complexType must have 'name' attribute",
559            None,
560        ));
561    }
562
563    if !ctx.is_top_level && has_name {
564        return Err(SchemaError::structural(
565            "src-ct",
566            "Inline complexType cannot have 'name' attribute",
567            None,
568        ));
569    }
570
571    if let Some(name_val) = attrs.get_value_by_name(name_table, "name") {
572        if !is_ncname(&collapsed(name_val)) {
573            return Err(SchemaError::structural(
574                "src-ct",
575                format!(
576                    "complexType 'name' value '{}' is not a valid NCName",
577                    name_val
578                ),
579                None,
580            ));
581        }
582    }
583
584    // §3.4.2 complexType/@final and @block allow `extension|restriction|#all`.
585    if let Some(final_val) = attrs.get_value_by_name(name_table, "final") {
586        validate_derivation_set_tokens(
587            final_val,
588            &["extension", "restriction"],
589            "final",
590            "complexType",
591        )?;
592    }
593    if let Some(block_val) = attrs.get_value_by_name(name_table, "block") {
594        validate_derivation_set_tokens(
595            block_val,
596            &["extension", "restriction"],
597            "block",
598            "complexType",
599        )?;
600    }
601
602    Ok(())
603}
604
605// ============================================================================
606// Derivation Validation
607// ============================================================================
608
609/// Validate restriction element structure
610///
611/// - Must have `base` attribute XOR inline type definition
612pub fn validate_restriction_structure(
613    attrs: &AttributeMap,
614    name_table: &NameTable,
615    has_inline_type: bool,
616) -> SchemaResult<()> {
617    let has_base = attrs.get_value_by_name(name_table, "base").is_some();
618
619    if has_base && has_inline_type {
620        return Err(SchemaError::structural(
621            "src-restriction-base-or-simpleType",
622            "Restriction cannot have both 'base' attribute and inline type",
623            None,
624        ));
625    }
626
627    // Note: In simple type restriction, base is required unless inline type exists
628    // This validation may need to be context-specific
629
630    Ok(())
631}
632
633/// Validate extension element structure
634///
635/// - Must have `base` attribute
636pub fn validate_extension_structure(
637    attrs: &AttributeMap,
638    name_table: &NameTable,
639) -> SchemaResult<()> {
640    let has_base = attrs.get_value_by_name(name_table, "base").is_some();
641
642    if !has_base {
643        return Err(SchemaError::structural(
644            "src-ct",
645            "Extension must have 'base' attribute",
646            None,
647        ));
648    }
649
650    Ok(())
651}
652
653// ============================================================================
654// List and Union Validation
655// ============================================================================
656
657/// Validate list element structure
658///
659/// - Must have `itemType` attribute XOR inline simpleType child
660pub fn validate_list_structure(
661    attrs: &AttributeMap,
662    name_table: &NameTable,
663    has_inline_type: bool,
664) -> SchemaResult<()> {
665    let has_item_type = attrs.get_value_by_name(name_table, "itemType").is_some();
666
667    if has_item_type && has_inline_type {
668        return Err(SchemaError::structural(
669            "src-list-itemType-or-simpleType",
670            "List cannot have both 'itemType' attribute and inline simpleType",
671            None,
672        ));
673    }
674
675    if !has_item_type && !has_inline_type {
676        return Err(SchemaError::structural(
677            "src-list-itemType-or-simpleType",
678            "List must have either 'itemType' attribute or inline simpleType",
679            None,
680        ));
681    }
682
683    Ok(())
684}
685
686/// Validate union element structure
687///
688/// - Must have `memberTypes` attribute and/or inline simpleType children
689pub fn validate_union_structure(
690    attrs: &AttributeMap,
691    name_table: &NameTable,
692    has_inline_types: bool,
693) -> SchemaResult<()> {
694    let has_member_types = attrs.get_value_by_name(name_table, "memberTypes").is_some();
695
696    if !has_member_types && !has_inline_types {
697        return Err(SchemaError::structural(
698            "src-union-memberTypes-or-simpleTypes",
699            "Union must have 'memberTypes' attribute or inline simpleType children",
700            None,
701        ));
702    }
703
704    Ok(())
705}
706
707// ============================================================================
708// Identity Constraint Validation
709// ============================================================================
710
711/// Validate key/unique element structure
712///
713/// - Must have `name` attribute
714/// - Child requirements (selector/field) are validated when the frame finishes
715pub fn validate_key_unique_structure(
716    attrs: &AttributeMap,
717    name_table: &NameTable,
718) -> SchemaResult<()> {
719    let has_name = attrs.get_value_by_name(name_table, "name").is_some();
720    let has_ref = attrs.get_value_by_name(name_table, "ref").is_some();
721
722    // §3.11.6 clause 1: one of @name or @ref must be present (but not both)
723    if !has_name && !has_ref {
724        return Err(SchemaError::structural(
725            "src-identity-constraint",
726            "Identity constraint (key/unique) must have 'name' or 'ref' attribute",
727            None,
728        ));
729    }
730
731    if let Some(name_val) = attrs.get_value_by_name(name_table, "name") {
732        if !is_ncname(&collapsed(name_val)) {
733            return Err(SchemaError::structural(
734                "src-identity-constraint",
735                format!(
736                    "identity constraint 'name' value '{}' is not a valid NCName",
737                    name_val
738                ),
739                None,
740            ));
741        }
742    }
743
744    Ok(())
745}
746
747/// Validate keyref element structure
748///
749/// - Must have `name` or `ref` attribute
750/// - `refer` is required when `name` is present
751/// - Child requirements (selector/field) are validated when the frame finishes
752pub fn validate_keyref_structure(attrs: &AttributeMap, name_table: &NameTable) -> SchemaResult<()> {
753    let has_name = attrs.get_value_by_name(name_table, "name").is_some();
754    let has_refer = attrs.get_value_by_name(name_table, "refer").is_some();
755    let has_ref = attrs.get_value_by_name(name_table, "ref").is_some();
756
757    // §3.11.6 clause 1: one of @name or @ref must be present
758    if !has_name && !has_ref {
759        return Err(SchemaError::structural(
760            "src-identity-constraint",
761            "Keyref must have 'name' or 'ref' attribute",
762            None,
763        ));
764    }
765
766    // §3.11.6 clause 3: @refer required when @name is present
767    if has_name && !has_refer {
768        return Err(SchemaError::structural(
769            "src-identity-constraint",
770            "Keyref must have 'refer' attribute",
771            None,
772        ));
773    }
774
775    if let Some(name_val) = attrs.get_value_by_name(name_table, "name") {
776        if !is_ncname(&collapsed(name_val)) {
777            return Err(SchemaError::structural(
778                "src-identity-constraint",
779                format!("keyref 'name' value '{}' is not a valid NCName", name_val),
780                None,
781            ));
782        }
783    }
784
785    Ok(())
786}
787
788// ============================================================================
789// Group Validation
790// ============================================================================
791
792/// Validate model group (group) element structure
793///
794/// - Top-level: `name` required
795/// - Reference: `ref` required, `name` prohibited
796pub fn validate_group_structure(
797    attrs: &AttributeMap,
798    name_table: &NameTable,
799    ctx: &ValidationContext,
800) -> SchemaResult<()> {
801    let has_name = attrs.get_value_by_name(name_table, "name").is_some();
802    let has_ref = attrs.get_value_by_name(name_table, "ref").is_some();
803
804    if ctx.is_top_level {
805        if !has_name {
806            return Err(SchemaError::structural(
807                "mgd-props-correct",
808                "Top-level group must have 'name' attribute",
809                None,
810            ));
811        }
812        if has_ref {
813            return Err(SchemaError::structural(
814                "mgd-props-correct",
815                "Top-level group cannot have 'ref' attribute",
816                None,
817            ));
818        }
819        // Top-level (named) group has no minOccurs/maxOccurs in its XML representation.
820        for prohibited in &["minOccurs", "maxOccurs"] {
821            if attrs.get_value_by_name(name_table, prohibited).is_some() {
822                return Err(SchemaError::structural(
823                    "mgd-props-correct",
824                    format!("Top-level group cannot have '{}' attribute", prohibited),
825                    None,
826                ));
827            }
828        }
829    } else {
830        // Non-top-level group: only `ref` is allowed; `name` is prohibited
831        // (XML Representation of <group>: ref form has no `name`).
832        if has_name {
833            return Err(SchemaError::structural(
834                "mgd-props-correct",
835                "Non-top-level group must use 'ref', not 'name'",
836                None,
837            ));
838        }
839        if !has_ref {
840            return Err(SchemaError::structural(
841                "mgd-props-correct",
842                "Non-top-level group must have 'ref' attribute",
843                None,
844            ));
845        }
846    }
847
848    if let Some(name_val) = attrs.get_value_by_name(name_table, "name") {
849        if !is_ncname(&collapsed(name_val)) {
850            return Err(SchemaError::structural(
851                "mgd-props-correct",
852                format!("group 'name' value '{}' is not a valid NCName", name_val),
853                None,
854            ));
855        }
856    }
857
858    Ok(())
859}
860
861/// Validate attribute group element structure
862///
863/// - Top-level: `name` required
864/// - Reference: `ref` required, `name` prohibited
865pub fn validate_attribute_group_structure(
866    attrs: &AttributeMap,
867    name_table: &NameTable,
868    ctx: &ValidationContext,
869) -> SchemaResult<()> {
870    let has_name = attrs.get_value_by_name(name_table, "name").is_some();
871    let has_ref = attrs.get_value_by_name(name_table, "ref").is_some();
872
873    if ctx.is_top_level {
874        if !has_name {
875            return Err(SchemaError::structural(
876                "src-attribute_group",
877                "Top-level attributeGroup must have 'name' attribute",
878                None,
879            ));
880        }
881        if has_ref {
882            return Err(SchemaError::structural(
883                "src-attribute_group",
884                "Top-level attributeGroup cannot have 'ref' attribute",
885                None,
886            ));
887        }
888    } else {
889        // Non-top-level attributeGroup: only `ref` is allowed; `name` is prohibited
890        // (XML Representation of <attributeGroup>: ref form has no `name`).
891        if has_name {
892            return Err(SchemaError::structural(
893                "src-attribute_group",
894                "Non-top-level attributeGroup must use 'ref', not 'name'",
895                None,
896            ));
897        }
898        if !has_ref {
899            return Err(SchemaError::structural(
900                "src-attribute_group",
901                "Non-top-level attributeGroup must have 'ref' attribute",
902                None,
903            ));
904        }
905    }
906
907    if let Some(name_val) = attrs.get_value_by_name(name_table, "name") {
908        if !is_ncname(&collapsed(name_val)) {
909            return Err(SchemaError::structural(
910                "src-attribute_group",
911                format!(
912                    "attributeGroup 'name' value '{}' is not a valid NCName",
913                    name_val
914                ),
915                None,
916            ));
917        }
918    }
919
920    Ok(())
921}
922
923// ============================================================================
924// XSD 1.1 Feature Gates
925// ============================================================================
926
927/// XSD 1.1 element names that are not allowed in XSD 1.0 mode
928pub const XSD_1_1_ELEMENTS: &[&str] = &[
929    "assert",
930    "assertion",
931    "alternative",
932    "openContent",
933    "defaultOpenContent",
934    "override",
935    "explicitTimezone",
936];
937
938/// XSD 1.1 attribute names that are not allowed in XSD 1.0 mode
939pub const XSD_1_1_ATTRIBUTES: &[&str] = &[
940    "targetNamespace",        // on element/attribute (local)
941    "notNamespace",           // on any/anyAttribute
942    "notQName",               // on any/anyAttribute
943    "inheritable",            // on attribute
944    "defaultAttributes",      // on schema
945    "defaultAttributesApply", // on complexType
946    "xpathDefaultNamespace",  // on schema/type definitions
947];
948
949/// Validate that an element is allowed in the current XSD version
950pub fn validate_xsd_version_element(
951    element_name: &str,
952    ctx: &ValidationContext,
953) -> SchemaResult<()> {
954    if ctx.xsd_version == XsdVersion::V1_0 && XSD_1_1_ELEMENTS.contains(&element_name) {
955        return Err(SchemaError::feature(
956            format!(
957                "Element '{}' requires XSD 1.1 but schema is in XSD 1.0 mode",
958                element_name
959            ),
960            None,
961        ));
962    }
963    Ok(())
964}
965
966/// Validate that an attribute is allowed in the current XSD version
967pub fn validate_xsd_version_attribute(
968    attr_name: &str,
969    element_name: &str,
970    ctx: &ValidationContext,
971) -> SchemaResult<()> {
972    if ctx.xsd_version == XsdVersion::V1_0 {
973        // Some XSD 1.1 attributes are context-specific
974        let is_xsd_1_1_attr = match (element_name, attr_name) {
975            ("element", "targetNamespace") => true,
976            ("attribute", "targetNamespace") => true,
977            ("attribute", "inheritable") => true,
978            ("complexType", "defaultAttributesApply") => true,
979            ("complexType", "xpathDefaultNamespace") => true,
980            ("any", "notNamespace") | ("any", "notQName") => true,
981            ("anyAttribute", "notNamespace") | ("anyAttribute", "notQName") => true,
982            ("schema", "defaultAttributes") => true,
983            ("schema", "xpathDefaultNamespace") => true,
984            ("selector", "xpathDefaultNamespace") => true,
985            ("field", "xpathDefaultNamespace") => true,
986            ("unique", "ref") | ("key", "ref") | ("keyref", "ref") => true,
987            // targetNamespace on schema is valid in XSD 1.0
988            ("schema", "targetNamespace") => false,
989            _ => XSD_1_1_ATTRIBUTES.contains(&attr_name),
990        };
991
992        if is_xsd_1_1_attr {
993            return Err(SchemaError::feature(
994                format!(
995                    "Attribute '{}' on '{}' requires XSD 1.1 but schema is in XSD 1.0 mode",
996                    attr_name, element_name
997                ),
998                None,
999            ));
1000        }
1001    }
1002    Ok(())
1003}
1004
1005// ============================================================================
1006// Notation Validation
1007// ============================================================================
1008
1009/// Validate notation declaration structure
1010///
1011/// - Must have `name` attribute
1012/// - Must have `public` attribute (XSD 1.0) or `public` or `system` (XSD 1.1)
1013pub fn validate_notation_structure(
1014    attrs: &AttributeMap,
1015    name_table: &NameTable,
1016    ctx: &ValidationContext,
1017) -> SchemaResult<()> {
1018    let has_name = attrs.get_value_by_name(name_table, "name").is_some();
1019    let has_public = attrs.get_value_by_name(name_table, "public").is_some();
1020    let has_system = attrs.get_value_by_name(name_table, "system").is_some();
1021
1022    if !has_name {
1023        return Err(SchemaError::structural(
1024            "n-props-correct",
1025            "Notation must have 'name' attribute",
1026            None,
1027        ));
1028    }
1029
1030    if let Some(name_val) = attrs.get_value_by_name(name_table, "name") {
1031        if !is_ncname(&collapsed(name_val)) {
1032            return Err(SchemaError::structural(
1033                "n-props-correct",
1034                format!("notation 'name' value '{}' is not a valid NCName", name_val),
1035                None,
1036            ));
1037        }
1038    }
1039
1040    match ctx.xsd_version {
1041        XsdVersion::V1_0 => {
1042            if !has_public {
1043                return Err(SchemaError::structural(
1044                    "n-props-correct",
1045                    "Notation must have 'public' attribute in XSD 1.0",
1046                    None,
1047                ));
1048            }
1049        }
1050        XsdVersion::V1_1 => {
1051            if !has_public && !has_system {
1052                return Err(SchemaError::structural(
1053                    "n-props-correct",
1054                    "Notation must have 'public' or 'system' attribute in XSD 1.1",
1055                    None,
1056                ));
1057            }
1058        }
1059    }
1060
1061    Ok(())
1062}
1063
1064// ============================================================================
1065// Include/Import/Redefine Validation
1066// ============================================================================
1067
1068/// Validate include directive structure
1069///
1070/// - Must have `schemaLocation` attribute
1071pub fn validate_include_structure(
1072    attrs: &AttributeMap,
1073    name_table: &NameTable,
1074) -> SchemaResult<()> {
1075    let has_location = attrs
1076        .get_value_by_name(name_table, "schemaLocation")
1077        .is_some();
1078
1079    if !has_location {
1080        return Err(SchemaError::structural(
1081            "src-include",
1082            "Include must have 'schemaLocation' attribute",
1083            None,
1084        ));
1085    }
1086
1087    Ok(())
1088}
1089
1090/// Validate import directive structure
1091///
1092/// - `schemaLocation` is optional
1093/// - `namespace`, when present, must not be the empty string (§4.2.6.2 / §4.2.3
1094///   src-import constraint 1.2 in XSD 1.1; §4.2.3 in XSD 1.0). Empty namespace is
1095///   forbidden because absent and "" denote different things in XSD.
1096pub fn validate_import_structure(attrs: &AttributeMap, name_table: &NameTable) -> SchemaResult<()> {
1097    if let Some(ns) = attrs.get_value_by_name(name_table, "namespace") {
1098        if ns.is_empty() {
1099            return Err(SchemaError::structural(
1100                "src-import",
1101                "xs:import 'namespace' must not be the empty string",
1102                None,
1103            ));
1104        }
1105    }
1106    Ok(())
1107}
1108
1109/// Validate xs:schema document structure
1110///
1111/// - `targetNamespace`, when present, must not be the empty string
1112///   (Schema Representation Constraint: targetNamespace cannot be empty per
1113///   §3.1.6 / §3.1.5).
1114///
1115/// Note: schema `targetNamespace = XSI namespace` is technically reserved per
1116/// §3.16.2 but the MS suite includes both `valid` (`attKb018`) and `invalid`
1117/// (`attKb018a`) outcomes for the same schema. The narrower check —
1118/// rejecting individual attribute declarations in the XSI namespace — is
1119/// implemented in `validate_no_xsi_attribute_declarations` and that aligns
1120/// with both interpretations.
1121pub fn validate_schema_structure(attrs: &AttributeMap, name_table: &NameTable) -> SchemaResult<()> {
1122    if let Some(tns) = attrs.get_value_by_name(name_table, "targetNamespace") {
1123        if tns.is_empty() {
1124            return Err(SchemaError::structural(
1125                "sch-props-correct",
1126                "xs:schema 'targetNamespace' must not be the empty string",
1127                None,
1128            ));
1129        }
1130    }
1131    Ok(())
1132}
1133
1134/// Validate redefine directive structure
1135///
1136/// - Must have `schemaLocation` attribute
1137pub fn validate_redefine_structure(
1138    attrs: &AttributeMap,
1139    name_table: &NameTable,
1140) -> SchemaResult<()> {
1141    let has_location = attrs
1142        .get_value_by_name(name_table, "schemaLocation")
1143        .is_some();
1144
1145    if !has_location {
1146        return Err(SchemaError::structural(
1147            "src-redefine",
1148            "Redefine must have 'schemaLocation' attribute",
1149            None,
1150        ));
1151    }
1152
1153    Ok(())
1154}
1155
1156#[cfg(test)]
1157mod tests {
1158    use super::*;
1159    use crate::parser::attrs::ParsedAttribute;
1160
1161    fn make_attr_map(name_table: &mut NameTable, attrs: &[(&str, &str)]) -> AttributeMap {
1162        let parsed: Vec<ParsedAttribute> = attrs
1163            .iter()
1164            .map(|(name, value)| ParsedAttribute {
1165                namespace: None,
1166                local_name: name_table.add(name),
1167                prefix: None,
1168                value: value.to_string(),
1169                source: None,
1170            })
1171            .collect();
1172        AttributeMap::new(parsed)
1173    }
1174
1175    #[test]
1176    fn test_element_top_level_valid() {
1177        let mut name_table = NameTable::new();
1178        let attrs = make_attr_map(&mut name_table, &[("name", "myElement")]);
1179        let ctx = ValidationContext::new(XsdVersion::V1_0, true);
1180
1181        let result = validate_element_structure(&attrs, &name_table, &ctx);
1182        assert!(result.is_ok());
1183    }
1184
1185    #[test]
1186    fn test_element_top_level_missing_name() {
1187        let mut name_table = NameTable::new();
1188        let attrs = make_attr_map(&mut name_table, &[("type", "xs:string")]);
1189        let ctx = ValidationContext::new(XsdVersion::V1_0, true);
1190
1191        let result = validate_element_structure(&attrs, &name_table, &ctx);
1192        assert!(result.is_err());
1193    }
1194
1195    #[test]
1196    fn test_element_top_level_has_ref() {
1197        let mut name_table = NameTable::new();
1198        let attrs = make_attr_map(&mut name_table, &[("name", "myElement"), ("ref", "other")]);
1199        let ctx = ValidationContext::new(XsdVersion::V1_0, true);
1200
1201        let result = validate_element_structure(&attrs, &name_table, &ctx);
1202        assert!(result.is_err());
1203    }
1204
1205    #[test]
1206    fn test_element_local_name_and_ref() {
1207        let mut name_table = NameTable::new();
1208        let attrs = make_attr_map(&mut name_table, &[("name", "myElement"), ("ref", "other")]);
1209        let ctx = ValidationContext::new(XsdVersion::V1_0, false);
1210
1211        let result = validate_element_structure(&attrs, &name_table, &ctx);
1212        assert!(result.is_err());
1213    }
1214
1215    #[test]
1216    fn test_element_default_and_fixed() {
1217        let mut name_table = NameTable::new();
1218        let attrs = make_attr_map(
1219            &mut name_table,
1220            &[("name", "myElement"), ("default", "a"), ("fixed", "b")],
1221        );
1222        let ctx = ValidationContext::new(XsdVersion::V1_0, true);
1223
1224        let result = validate_element_structure(&attrs, &name_table, &ctx);
1225        assert!(result.is_err());
1226    }
1227
1228    #[test]
1229    fn test_attribute_prohibited_with_default() {
1230        let mut name_table = NameTable::new();
1231        let attrs = make_attr_map(
1232            &mut name_table,
1233            &[("ref", "myAttr"), ("use", "prohibited"), ("default", "x")],
1234        );
1235        let ctx = ValidationContext::new(XsdVersion::V1_0, false);
1236
1237        let result = validate_attribute_structure(&attrs, &name_table, &ctx);
1238        assert!(result.is_err());
1239    }
1240
1241    #[test]
1242    fn test_xsd_1_1_element_in_1_0_mode() {
1243        let ctx = ValidationContext::new(XsdVersion::V1_0, false);
1244        let result = validate_xsd_version_element("assert", &ctx);
1245        assert!(result.is_err());
1246    }
1247
1248    #[test]
1249    fn test_xsd_1_1_element_in_1_1_mode() {
1250        let ctx = ValidationContext::new(XsdVersion::V1_1, false);
1251        let result = validate_xsd_version_element("assert", &ctx);
1252        assert!(result.is_ok());
1253    }
1254
1255    #[test]
1256    fn test_keyref_requires_refer() {
1257        let mut name_table = NameTable::new();
1258        let attrs = make_attr_map(&mut name_table, &[("name", "myKeyRef")]);
1259
1260        let result = validate_keyref_structure(&attrs, &name_table);
1261        assert!(result.is_err());
1262    }
1263
1264    #[test]
1265    fn test_keyref_with_refer() {
1266        let mut name_table = NameTable::new();
1267        let attrs = make_attr_map(&mut name_table, &[("name", "myKeyRef"), ("refer", "myKey")]);
1268
1269        let result = validate_keyref_structure(&attrs, &name_table);
1270        assert!(result.is_ok());
1271    }
1272
1273    #[test]
1274    fn test_list_itemtype_and_inline() {
1275        let mut name_table = NameTable::new();
1276        let attrs = make_attr_map(&mut name_table, &[("itemType", "xs:string")]);
1277
1278        // Has both itemType and inline type - should fail
1279        let result = validate_list_structure(&attrs, &name_table, true);
1280        assert!(result.is_err());
1281    }
1282
1283    #[test]
1284    fn test_list_neither_itemtype_nor_inline() {
1285        let mut name_table = NameTable::new();
1286        let attrs = make_attr_map(&mut name_table, &[]);
1287
1288        let result = validate_list_structure(&attrs, &name_table, false);
1289        assert!(result.is_err());
1290    }
1291
1292    #[test]
1293    fn test_extension_requires_base() {
1294        let mut name_table = NameTable::new();
1295        let attrs = make_attr_map(&mut name_table, &[]);
1296
1297        let result = validate_extension_structure(&attrs, &name_table);
1298        assert!(result.is_err());
1299    }
1300
1301    #[test]
1302    fn test_notation_requires_public_in_1_0() {
1303        let mut name_table = NameTable::new();
1304        let attrs = make_attr_map(
1305            &mut name_table,
1306            &[("name", "myNotation"), ("system", "foo")],
1307        );
1308        let ctx = ValidationContext::new(XsdVersion::V1_0, true);
1309
1310        let result = validate_notation_structure(&attrs, &name_table, &ctx);
1311        assert!(result.is_err());
1312    }
1313
1314    #[test]
1315    fn test_notation_system_ok_in_1_1() {
1316        let mut name_table = NameTable::new();
1317        let attrs = make_attr_map(
1318            &mut name_table,
1319            &[("name", "myNotation"), ("system", "foo")],
1320        );
1321        let ctx = ValidationContext::new(XsdVersion::V1_1, true);
1322
1323        let result = validate_notation_structure(&attrs, &name_table, &ctx);
1324        assert!(result.is_ok());
1325    }
1326
1327    // --- xpathDefaultNamespace version gating tests ---
1328
1329    #[test]
1330    fn test_xpath_default_ns_on_selector_rejected_in_1_0() {
1331        let ctx = ValidationContext::new(XsdVersion::V1_0, false);
1332        let result = validate_xsd_version_attribute("xpathDefaultNamespace", "selector", &ctx);
1333        assert!(result.is_err());
1334    }
1335
1336    #[test]
1337    fn test_xpath_default_ns_on_field_rejected_in_1_0() {
1338        let ctx = ValidationContext::new(XsdVersion::V1_0, false);
1339        let result = validate_xsd_version_attribute("xpathDefaultNamespace", "field", &ctx);
1340        assert!(result.is_err());
1341    }
1342
1343    #[test]
1344    fn test_xpath_default_ns_on_schema_rejected_in_1_0() {
1345        let ctx = ValidationContext::new(XsdVersion::V1_0, true);
1346        let result = validate_xsd_version_attribute("xpathDefaultNamespace", "schema", &ctx);
1347        assert!(result.is_err());
1348    }
1349
1350    #[test]
1351    fn test_target_namespace_on_schema_allowed_in_1_0() {
1352        let ctx = ValidationContext::new(XsdVersion::V1_0, true);
1353        let result = validate_xsd_version_attribute("targetNamespace", "schema", &ctx);
1354        assert!(result.is_ok());
1355    }
1356
1357    #[test]
1358    fn test_xsd_1_0_rejects_default_attributes_on_schema() {
1359        let ctx = ValidationContext::new(XsdVersion::V1_0, true);
1360        let result = validate_xsd_version_attribute("defaultAttributes", "schema", &ctx);
1361        assert!(result.is_err());
1362
1363        // Allowed in XSD 1.1
1364        let ctx11 = ValidationContext::new(XsdVersion::V1_1, true);
1365        let result11 = validate_xsd_version_attribute("defaultAttributes", "schema", &ctx11);
1366        assert!(result11.is_ok());
1367    }
1368
1369    #[test]
1370    fn test_xpath_default_ns_on_selector_allowed_in_1_1() {
1371        let ctx = ValidationContext::new(XsdVersion::V1_1, false);
1372        let result = validate_xsd_version_attribute("xpathDefaultNamespace", "selector", &ctx);
1373        assert!(result.is_ok());
1374    }
1375
1376    #[test]
1377    fn test_xpath_default_ns_on_field_allowed_in_1_1() {
1378        let ctx = ValidationContext::new(XsdVersion::V1_1, false);
1379        let result = validate_xsd_version_attribute("xpathDefaultNamespace", "field", &ctx);
1380        assert!(result.is_ok());
1381    }
1382}