Skip to content

Commit a7e3e5e

Browse files
committed
xsd: add elementFormDefault support for namespace-qualified validation
Parse elementFormDefault="qualified" from the schema root element. When set, child elements in instance documents must have the schema's target namespace. Add element_matches_decl() helper used by sequence, choice, and all content model validation. Fixes namespace validation for UBL 2.4 and similar schemas that require qualified local elements.
1 parent ad0c36c commit a7e3e5e

1 file changed

Lines changed: 127 additions & 6 deletions

File tree

src/validation/xsd.rs

Lines changed: 127 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,24 @@ pub struct XsdSchema {
125125
/// Used during validation to resolve `QName` type references like
126126
/// `tns:AddressType` to the correct namespace for imported type lookup.
127127
prefix_map: HashMap<String, String>,
128+
/// The `elementFormDefault` attribute from the schema root.
129+
///
130+
/// When `Qualified`, local element declarations must be namespace-qualified
131+
/// in instance documents. Default is `Unqualified`.
132+
///
133+
/// See XSD 1.0 section 3.3.2.
134+
element_form_default: FormDefault,
135+
}
136+
137+
/// Whether local elements/attributes must be namespace-qualified in instances.
138+
///
139+
/// See XSD 1.0 section 3.3.2.
140+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141+
pub enum FormDefault {
142+
/// Local elements do not need to be namespace-qualified (default).
143+
Unqualified,
144+
/// Local elements must be namespace-qualified in instance documents.
145+
Qualified,
128146
}
129147

130148
/// Declarations imported from another namespace via `xsd:import`.
@@ -437,6 +455,10 @@ pub fn parse_xsd_with_options(
437455
column: None,
438456
})?;
439457
let prefix_map = build_prefix_map(&root_doc, root_elem);
458+
let element_form_default = match root_doc.attribute(root_elem, "elementFormDefault") {
459+
Some("qualified") => FormDefault::Qualified,
460+
_ => FormDefault::Unqualified,
461+
};
440462

441463
let mut schema = XsdSchema {
442464
target_namespace: None,
@@ -445,6 +467,7 @@ pub fn parse_xsd_with_options(
445467
attribute_groups: HashMap::new(),
446468
imported_namespaces: HashMap::new(),
447469
prefix_map,
470+
element_form_default,
448471
};
449472

450473
register_builtin_types(&mut schema);
@@ -712,13 +735,18 @@ fn handle_import(
712735
};
713736

714737
// We need a temporary XsdSchema to parse into, then extract declarations
738+
let imported_form_default = match imported_doc.attribute(imported_root, "elementFormDefault") {
739+
Some("qualified") => FormDefault::Qualified,
740+
_ => FormDefault::Unqualified,
741+
};
715742
let mut temp_schema = XsdSchema {
716743
target_namespace: Some(ns_key.clone()),
717744
elements: HashMap::new(),
718745
types: HashMap::new(),
719746
attribute_groups: HashMap::new(),
720747
imported_namespaces: HashMap::new(),
721748
prefix_map: build_prefix_map(&imported_doc, imported_root),
749+
element_form_default: imported_form_default,
722750
};
723751
register_builtin_types(&mut temp_schema);
724752
parse_top_level_declarations(
@@ -1410,6 +1438,32 @@ fn validate_sequence(
14101438
}
14111439

14121440
/// Validates a single element particle in a sequence, returning number consumed.
1441+
/// Checks if an instance element matches a schema element declaration,
1442+
/// accounting for `elementFormDefault` and element-level `form` attributes.
1443+
///
1444+
/// When qualified form is in effect, the element must have the schema's
1445+
/// target namespace. When unqualified, the element is matched by local
1446+
/// name only (no namespace required).
1447+
fn element_matches_decl(
1448+
doc: &Document,
1449+
node: NodeId,
1450+
decl: &XsdElement,
1451+
schema: &XsdSchema,
1452+
) -> bool {
1453+
let child_name = doc.node_name(node).unwrap_or("");
1454+
if child_name != decl.name {
1455+
return false;
1456+
}
1457+
// Check namespace qualification
1458+
if schema.element_form_default == FormDefault::Qualified {
1459+
if let Some(ref target_ns) = schema.target_namespace {
1460+
let child_ns = doc.node_namespace(node).unwrap_or("");
1461+
return child_ns == target_ns;
1462+
}
1463+
}
1464+
true
1465+
}
1466+
14131467
fn validate_sequence_element(
14141468
doc: &Document,
14151469
children: &[NodeId],
@@ -1421,8 +1475,7 @@ fn validate_sequence_element(
14211475
let mut count: u32 = 0;
14221476
let mut consumed = 0;
14231477
for &child in children {
1424-
let child_name = doc.node_name(child).unwrap_or("");
1425-
if child_name != decl.name {
1478+
if !element_matches_decl(doc, child, decl, schema) {
14261479
break;
14271480
}
14281481
if let MaxOccurs::Bounded(max) = decl.max_occurs {
@@ -1499,7 +1552,7 @@ fn validate_choice(
14991552
let first_name = doc.node_name(first).unwrap_or("");
15001553
let matched = particles.iter().any(|p| {
15011554
if let XsdParticle::Element(decl) = p {
1502-
if decl.name == first_name {
1555+
if element_matches_decl(doc, first, decl, schema) {
15031556
validate_element(doc, first, decl, schema, errors);
15041557
return true;
15051558
}
@@ -1536,9 +1589,9 @@ fn validate_all(
15361589
let mut seen: HashMap<&str, u32> = HashMap::new();
15371590
for &child in children {
15381591
let child_name = doc.node_name(child).unwrap_or("");
1539-
let matching = particles
1540-
.iter()
1541-
.find(|p| matches!(p, XsdParticle::Element(d) if d.name == child_name));
1592+
let matching = particles.iter().find(
1593+
|p| matches!(p, XsdParticle::Element(d) if element_matches_decl(doc, child, d, schema)),
1594+
);
15421595
if let Some(XsdParticle::Element(decl)) = matching {
15431596
let count = seen.entry(child_name).or_insert(0);
15441597
*count += 1;
@@ -3611,4 +3664,72 @@ mod tests {
36113664
let result2 = validate_xsd(&doc2, &schema);
36123665
assert!(!result2.is_valid);
36133666
}
3667+
3668+
#[test]
3669+
fn test_element_form_default_qualified() {
3670+
let schema = parse_xsd(
3671+
r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
3672+
targetNamespace="urn:example"
3673+
xmlns:tns="urn:example"
3674+
elementFormDefault="qualified">
3675+
<xs:element name="order">
3676+
<xs:complexType><xs:sequence>
3677+
<xs:element name="item" type="xs:string"/>
3678+
</xs:sequence></xs:complexType>
3679+
</xs:element>
3680+
</xs:schema>"#,
3681+
)
3682+
.unwrap();
3683+
assert_eq!(schema.element_form_default, FormDefault::Qualified);
3684+
3685+
// Valid: child element is namespace-qualified
3686+
let doc = Document::parse_str(r#"<order xmlns="urn:example"><item>Widget</item></order>"#)
3687+
.unwrap();
3688+
let result = validate_xsd(&doc, &schema);
3689+
assert!(
3690+
result.is_valid,
3691+
"qualified children should pass: {:?}",
3692+
result.errors
3693+
);
3694+
3695+
// Invalid: child element is NOT namespace-qualified
3696+
let doc_fail = Document::parse_str(
3697+
r#"<tns:order xmlns:tns="urn:example"><item>Widget</item></tns:order>"#,
3698+
)
3699+
.unwrap();
3700+
let result = validate_xsd(&doc_fail, &schema);
3701+
assert!(
3702+
!result.is_valid,
3703+
"unqualified child should fail when elementFormDefault=qualified"
3704+
);
3705+
}
3706+
3707+
#[test]
3708+
fn test_element_form_default_unqualified() {
3709+
let schema = parse_xsd(
3710+
r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
3711+
targetNamespace="urn:example"
3712+
xmlns:tns="urn:example">
3713+
<xs:element name="order">
3714+
<xs:complexType><xs:sequence>
3715+
<xs:element name="item" type="xs:string"/>
3716+
</xs:sequence></xs:complexType>
3717+
</xs:element>
3718+
</xs:schema>"#,
3719+
)
3720+
.unwrap();
3721+
assert_eq!(schema.element_form_default, FormDefault::Unqualified);
3722+
3723+
// Valid: child element without namespace (unqualified is default)
3724+
let doc = Document::parse_str(
3725+
r#"<tns:order xmlns:tns="urn:example"><item>Widget</item></tns:order>"#,
3726+
)
3727+
.unwrap();
3728+
let result = validate_xsd(&doc, &schema);
3729+
assert!(
3730+
result.is_valid,
3731+
"unqualified children should pass: {:?}",
3732+
result.errors
3733+
);
3734+
}
36143735
}

0 commit comments

Comments
 (0)