import Testing
@testable import PureXML
@Suite("XSD Particle Valid (Restriction)")
struct XSDParticleRestrictionTests {
private let xsNamespace = "xmlns:xs=\"http://www.w3.org/2001/XMLSchema\""
private func schema(base: String, restriction: String) -> String {
"""
\(base)
\(restriction)
"""
}
private func compiles(base: String, restriction: String) -> Bool {
(try? PureXML.Schema.Document(schema(base: base, restriction: restriction))) != nil
}
private func restrictionError(base: String, restriction: String) -> String? {
do {
_ = try PureXML.Schema.Document(schema(base: base, restriction: restriction))
return nil
} catch let error as PureXML.Schema.SchemaError {
return String(describing: error)
} catch {
return String(describing: error)
}
}
@Test("Valid restrictions compile: drop an optional, narrow a choice, keep order")
func test_validRestrictions() {
// Dropping an optional element.
#expect(compiles(
base: "",
restriction: "",
))
// Narrowing a choice to one branch.
#expect(compiles(
base: "",
restriction: "",
))
// Narrowing an occurrence range.
#expect(compiles(
base: "",
restriction: "",
))
// A maxOccurs=0 particle never occurs (pointless particle); it is removed
// before mapping, so it must not consume the base wildcard the next element
// restricts (particlesJd).
#expect(compiles(
base: "",
restriction: "",
))
// A content-free restriction (an empty sequence) of an EMPTY base is valid:
// both accept only the empty sequence (addB079).
#expect(compiles(
base: "",
restriction: "",
))
}
@Test("An all-maxOccurs=0 derived sequence does not vacuously restrict a required base")
func test_emptyDerivedRequiredBase() {
// After pointless-particle removal the derived sequence is empty (accepts only
// the empty sequence); restricting a required choice {a|b} is invalid.
#expect(restrictionError(
base: "",
restriction: "",
)?.contains("not a valid restriction") == true)
// But restricting an emptiable base choice {a?|b} is valid.
#expect(compiles(
base: "",
restriction: "",
))
}
@Test("A restriction widening the occurrence range is rejected")
func test_wideningOccurrenceRejected() {
let error = restrictionError(
base: "",
restriction: "",
)
#expect(error?.contains("not a valid restriction") == true, "\(String(describing: error))")
}
@Test("A restriction introducing a new element name is rejected")
func test_newNameRejected() {
let error = restrictionError(
base: "",
restriction: "",
)
#expect(error?.contains("not a valid restriction") == true)
}
@Test("A restriction reordering a sequence is rejected")
func test_reorderRejected() {
let error = restrictionError(
base: "",
restriction: "",
)
#expect(error?.contains("not a valid restriction") == true)
}
@Test("Restricting to empty requires an emptiable base")
func test_emptyRestriction() {
// Emptiable base: fine.
#expect(compiles(
base: "",
restriction: "",
))
// Required base content: rejected.
let error = restrictionError(
base: "",
restriction: "",
)
#expect(error?.contains("cannot be EMPTY") == true, "\(String(describing: error))")
}
@Test("An element restricts a wildcard only when its namespace is admitted")
func test_wildcardRestriction() {
// ##any admits the unqualified element.
#expect(compiles(
base: "",
restriction: "",
))
// ##other refuses an unqualified element.
let error = restrictionError(
base: "",
restriction: "",
)
#expect(error?.contains("not a valid restriction") == true)
}
@Test("A single-member derived sequence narrowing an occurrence restricts a wider base sequence")
func test_singleMemberSequenceNarrowsOccurrence() {
// particlesW008: the derived sequence has one member, so it normalizes to the
// bare element e1{2,3}; that must still be matched as a member of the base
// sequence (RecurseAsIfGroup), not have its {2,3} compared to the base
// sequence's own {1,1} occurrence.
#expect(compiles(
base: ""
+ ""
+ "",
restriction: "",
))
// particlesM002: a derived single-branch choice normalizes to the bare element
// and still restricts a choice of sequences whose first branch begins with it.
#expect(compiles(
base: ""
+ ""
+ "",
restriction: "",
))
}
@Test("A group restricting a wildcard is valid only when its effective count fits the wildcard's range")
func test_groupRestrictsWildcardCardinality() {
// particlesHa: a sequence of named elements restricts a wildcard when every leaf
// is admitted AND the effective total count lies within the wildcard's range.
#expect(compiles(
base: "",
restriction: "",
))
// Too many leaves: three elements exceed the wildcard's maxOccurs of 2.
#expect(restrictionError(
base: "",
restriction: "",
)?.contains("not a valid restriction") == true)
// Too few leaves: one element is below the wildcard's minOccurs of 2.
#expect(restrictionError(
base: "",
restriction: "",
)?.contains("not a valid restriction") == true)
}
@Test("A content-bearing derived group never restricts a base single element")
func test_groupRestrictsSingleElement() {
// mgE014: base sequence(title?) normalizes to the bare element title{0,1};
// the derived sequence(title{0,0}) normalizes to the empty (content-free)
// group, which accepts only the empty sequence, valid against an emptiable
// element. This is the content-free path, not a derived-group/base-element rule.
#expect(compiles(
base: "",
restriction: "",
))
// XSD 1.0 ยง3.9.6 defines NO restriction rule for a derived group against a base
// element (only the reverse, an element against a base group). A content-bearing
// derived group restricting a single base element is therefore invalid, even
// when its leaves share the element's name and its effective count fits.
#expect(restrictionError(
base: "",
restriction: "",
)?.contains("not a valid restriction") == true)
// The same holds when the derived group's effective count exceeds the base
// element's maxOccurs, and when a leaf is differently named.
#expect(restrictionError(
base: "",
restriction: "",
)?.contains("not a valid restriction") == true)
#expect(restrictionError(
base: "",
restriction: "",
)?.contains("not a valid restriction") == true)
}
@Test("Conformance: the restriction corpus passes through the harness")
func test_conformanceCases() {
let valid = compiles(
base: "",
restriction: "",
)
let invalid = compiles(
base: "",
restriction: "",
)
let cases = [
PureXML.Validation.ConformanceCase(name: "faithful-restriction-accepted", actual: valid ? "valid" : "invalid", expected: "valid"),
PureXML.Validation.ConformanceCase(name: "additive-restriction-rejected", actual: invalid ? "valid" : "invalid", expected: "invalid"),
]
let failures = PureXML.Validation.Conformance.failures(in: cases)
#expect(failures.isEmpty, "\(failures.map(\.reason))")
}
@Test("NameAndTypeOK: a same-name element must keep a type that derives from the base's")
func test_elementTypeDerivation() {
// Renaming the element's type to an incompatible one (string -> int) is not a
// valid restriction: int does not derive from string.
#expect(!compiles(
base: "",
restriction: "",
))
// Narrowing to a derived type (integer -> int) is valid.
#expect(compiles(
base: "",
restriction: "",
))
// Keeping the same type is valid.
#expect(compiles(
base: "",
restriction: "",
))
}
@Test("NameAndTypeOK: widening a concrete-typed element to the ur-type is rejected")
func test_elementUrTypeWidening() {
// Restricting a string element to anyType/anySimpleType is widening, invalid.
#expect(!compiles(
base: "",
restriction: "",
))
#expect(!compiles(
base: "",
restriction: "",
))
// The reverse (base is the ur-type, restriction narrows it) is valid.
#expect(compiles(
base: "",
restriction: "",
))
// A user type whose local name is "anyType" (not the XSD ur-type) must not be
// mistaken for it: this is a valid same-type restriction.
#expect((try? PureXML.Schema.Document("""
""")) != nil)
}
@Test("NameAndTypeOK over named user and union types")
func test_elementUserTypeDerivation() {
func doc(_ elementC1: String, base baseC1: String, types: String) -> PureXML.Schema.Document? {
try? PureXML.Schema.Document("""
\(types)
""")
}
let userTypes = """
"""
// bar restricts foo: a valid narrowing of a user type.
#expect(doc("bar", base: "foo", types: userTypes) != nil)
// foo restricting bar widens (foo is bar's base): invalid.
#expect(doc("foo", base: "bar", types: userTypes) == nil)
// two unrelated user types: invalid.
#expect(doc("other", base: "foo", types: userTypes) == nil)
// a union member (xs:string) validly restricts the union-typed base element.
#expect(doc("xs:string", base: "u", types: userTypes) != nil)
let unions = """
"""
// A separately-declared union whose members merely subset the base union's
// is NOT a valid derivation: cos-st-derived-ok requires the restricting
// type to be the base, derive from it through a declared restriction chain,
// or derive from one of its members. Structural member subsetting is not
// the spec's derivation relation (cf. XSTS particlesIk011).
#expect(doc("U1", base: "U2", types: unions) == nil)
// The reverse (U2 has a member U1 lacks) is invalid for the same reason.
#expect(doc("U2", base: "U1", types: unions) == nil)
// a list type restricting a union type: invalid.
#expect(doc("L1", base: "U1", types: unions) == nil)
}
}
@Suite("XSD element-reference namespace resolution")
struct XSDElementRefNamespaceTests {
@Test("An element reference resolves to its prefix's namespace, not the target namespace")
func test_elementRefNamespaceResolution() {
// particlesJj001: a ref to an imported element (imp -> http://importedXSD)
// restricts a `##other` wildcard. `##other` admits any namespace but the
// target, so an imported element is admitted and the restriction is valid;
// resolving the ref into the target namespace would make `##other` reject it.
let imported = ""
+ ""
+ ""
+ ""
+ ""
+ ""
#expect((try? PureXML.Schema.Document(imported)) != nil)
// The same shape with a ref into the TARGET namespace (a declared global) is
// invalid: `##other` forbids the target namespace, so the element is not
// admitted. This proves the resolved namespace is actually checked.
let targetNs = ""
+ ""
+ ""
+ ""
+ ""
+ ""
#expect((try? PureXML.Schema.Document(targetNs)) == nil)
}
@Test("An unprefixed ref resolves through the default namespace, qualifying an instance element")
func test_unprefixedRefUsesDefaultNamespace() {
// The spec-faithful common idiom: the target namespace is also the default
// xmlns, so a bare ref="child" resolves to the target namespace and a
// target-namespace instance element validates against it.
let schema = ""
+ ""
+ ""
+ ""
guard let document = try? PureXML.Schema.Document(schema) else {
Issue.record("schema should compile")
return
}
#expect((try? document.validate("x"))?.isEmpty == true)
// A child in the wrong namespace is rejected (the resolution is real, not a
// blanket accept).
let wrong = "x"
#expect((try? document.validate(wrong))?.isEmpty != true)
}
}