PureXML/Tests/SchemaNamespacedDerivationTests.swift
Mihaela Mihaljevic 1fe670581f feat(schema): key the instance derivation/substitution backbone by namespaced identity; re-land blockDefault; invalid-instances-accepted 89 to 81 (#186, #147)
The instance-validity derivation and substitution tables were keyed by bare
local name, so two same-local-name types or elements in different namespaces
collided. In multi-document validation this let one schema's block/derivation
facts shadow another's, and it blocked honouring blockDefault (whose re-land
widened that collision into an over-rejection).

Key the instance subsystem by namespaced identity {ns}local instead. The shared
DerivationTables keeps its bare-keyed fields for the schema-validity machinery
(Particle-Valid-Restriction and the consistency rules, which resolve QNames to
bare names and walk the same backbone) and gains parallel ns* fields built in
the same pass, resolving each container's components through its target
namespace and prefix bindings. Only the instance consumers
(blockedSubstitutionError, notDerivedSubstitutionError, abstractTypeError, the
substitution-group closure, and referenceParticle) read the ns* tables, via a
derivationKey(fromReference:) that reduces a type:{ns}local reference, a bare
name, or an element ref to a {ns}local backbone key. A key miss stays permissive
(no constraint, substitution allowed), so the re-keying cannot over-reject.

With the namespaced backbone in place, blockDefault is re-landed FP-free: a
schema's blockDefault supplies the {disallowed substitutions} of every element
and complex type without an explicit block (own block overrides; block="" clears;
intersected with the methods each component admits), so blockDefault="extension"
bars an extension substitution and "#all" bars all, without one namespace's
defaults shadowing another's.

The pure re-keying preserves single-document behaviour exactly (the corpus is
overwhelmingly single-namespace, where {ns}local and bare coincide): all four
XSTS buckets held at vsr=0 / isa=69 / vir=0 / ivi=89 after re-keying alone.
blockDefault then drove invalid-instances-accepted 89 to 81, no false positive
(valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket
moved. An adversarial review exercised same- and cross-namespace substitutions,
no-targetNamespace keys, simple-type restriction, transitive substitution groups,
and blockDefault override/over-block cases, and the prior multi-document
cross-namespace blockDefault over-rejection is now accepted while a same-namespace
blocked substitution is still rejected (precise, not merely permissive).
2026-06-18 12:41:18 +02:00

62 lines
2.9 KiB
Swift

@testable import PureXML
import Testing
/// The instance-validity derivation/substitution subsystem keys its `block`,
/// abstract-type, and derivation-backbone tables by namespaced identity
/// (`{ns}local`), so two complex types sharing a local name in different
/// namespaces do not collide. Without this, a `blockDefault` in one namespace
/// would wrongly block an `xsi:type` substitution among the same-named types of
/// another namespace (a false rejection).
@Suite("namespaced derivation keys do not cross-block")
struct SchemaNamespacedDerivationTests {
/// Schema A (`urn:a`, `blockDefault="#all"`) imports schema B (`urn:b`, no
/// block). Both declare a complex type `T`, a `TD` extending their own `T`, and
/// an element `e` typed by their own `T`.
private let schemaA = """
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="urn:a" xmlns:a="urn:a" blockDefault="#all">
<xs:import namespace="urn:b" schemaLocation="b.xsd"/>
<xs:complexType name="T"><xs:sequence><xs:element name="x" type="xs:string"/></xs:sequence></xs:complexType>
<xs:complexType name="TD">
<xs:complexContent><xs:extension base="a:T"><xs:sequence><xs:element name="y" type="xs:string"/></xs:sequence></xs:extension></xs:complexContent>
</xs:complexType>
<xs:element name="e" type="a:T"/>
</xs:schema>
"""
private let schemaB = """
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:b" xmlns:b="urn:b">
<xs:complexType name="T"><xs:sequence><xs:element name="x" type="xs:string"/></xs:sequence></xs:complexType>
<xs:complexType name="TD">
<xs:complexContent><xs:extension base="b:T"><xs:sequence><xs:element name="y" type="xs:string"/></xs:sequence></xs:extension></xs:complexContent>
</xs:complexType>
<xs:element name="e" type="b:T"/>
</xs:schema>
"""
private var loader: (String) -> String? {
let schemaB = schemaB
return { $0 == "b.xsd" ? schemaB : nil }
}
@Test("an extension xsi:type in urn:b is not blocked by urn:a's blockDefault")
func test_otherNamespaceNotBlocked() throws {
let document = try PureXML.Schema.Document(schemaA, schemaLoader: loader)
let instance = """
<b:e xmlns:b="urn:b" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:type="b:TD"><x>a</x><y>b</y></b:e>
"""
#expect(try document.validate(instance, schemaLoader: loader).isEmpty)
}
@Test("the same extension xsi:type in urn:a IS blocked by its own blockDefault")
func test_ownNamespaceBlocked() throws {
let document = try PureXML.Schema.Document(schemaA, schemaLoader: loader)
let instance = """
<a:e xmlns:a="urn:a" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:type="a:TD"><x>a</x><y>b</y></a:e>
"""
#expect(try !document.validate(instance, schemaLoader: loader).isEmpty)
}
}