@testable import PureXML
import Testing
/// Structural validity of a schema document against the schema-for-schemas
/// content model (XSD 1.0 Structures): each component's children must be
/// admitted by the model, an `annotation` (where allowed once) must be first,
/// and an identity constraint needs a selector and field. Such a schema is
/// invalid and must be rejected at compile time; the children were previously
/// unchecked and the schema accepted (XSTS invalid-schema ctB/ctG/ctH, etc.).
@Suite("Schema structural validity")
struct SchemaStructureTests {
private func compile(_ body: String) throws {
_ = try PureXML.Schema.Document("""
\(body)
""")
}
private func rejects(_ body: String) -> Bool {
do { try compile(body)
return false
} catch { return true }
}
@Test("well-formed components compile")
func test_valid() throws {
try compile(#"""
doc
"""#)
try compile(#"""
"""#)
}
@Test("a child not admitted by the content model is rejected")
func test_disallowedChild() {
// element is not a direct child of complexType (it must sit in a group).
#expect(rejects(#""#))
// attribute admits only annotation and simpleType, not sequence.
#expect(rejects(#""#))
}
@Test("at most one annotation, and it must be first")
func test_annotationPlacement() {
#expect(rejects(#"""
one
two
"""#))
#expect(rejects(#"""
late
"""#))
}
@Test("schema and redefine may carry several annotations")
func test_multipleAnnotationOnSchema() throws {
try compile(#"""
one
two
"""#)
}
@Test("enumerated attributes accept only their value space")
func test_enumeratedAttributeValues() {
#expect(rejects(#""#))
#expect(rejects(#""#))
#expect(rejects(#""#))
#expect(rejects(#""#))
#expect(rejects(#""#))
}
@Test("minOccurs and maxOccurs must be nonNegativeInteger (maxOccurs also unbounded)")
func test_occursValues() throws {
try compile(#""#)
#expect(rejects(#""#))
#expect(rejects(#""#))
#expect(rejects(#""#))
}
@Test("a particle's minOccurs may not exceed its maxOccurs")
func test_occurrenceOrder() throws {
func wrap(_ particle: String) -> String {
"\(particle)"
}
#expect(rejects(wrap(#""#)))
#expect(rejects(wrap(#""#)))
// Equal bounds, and unbounded, are fine.
try compile(wrap(#""#))
try compile(wrap(#""#))
try compile(wrap(#""#))
}
@Test("valid enumerated and occurrence attribute values compile")
func test_validAttributeValues() throws {
try compile(#""#)
try compile(#""#)
try compile(#""#)
}
@Test("a component name must be a valid NCName")
func test_nameNCName() {
#expect(rejects(#""#))
#expect(rejects(#""#))
#expect(rejects(#""#))
#expect(rejects(#""#))
try? compile(#""#)
}
@Test("a QName-valued reference attribute must be a lexical QName")
func test_qnameReferences() throws {
#expect(rejects(#""#))
#expect(rejects(#""#))
try compile(#""#)
try compile(#""#)
}
@Test("a complexType's content shape is exclusive")
func test_complexTypeContentExclusivity() {
// simpleContent/complexContent are mutually exclusive and exclude a model
// group or a direct attribute; only one model group is allowed.
#expect(rejects(#"""
"""#))
#expect(rejects(#"""
"""#))
#expect(rejects(#"""
"""#))
}
@Test("valid complexType content shapes compile")
func test_complexTypeContentValid() throws {
try compile(#"""
"""#)
try compile(#""#)
try compile(#""#)
}
@Test("a named group must contain exactly one model group")
func test_namedGroupContent() throws {
try compile(#""#)
// Two compositors, or none, is invalid.
#expect(rejects(#"""
"""#))
#expect(rejects(#"x"#))
// A group reference (no name) is not a definition and is not flagged here.
try compile(
#""#,
)
}
@Test("an attribute not admitted by a component is rejected")
func test_attributeApplicability() {
#expect(rejects(#""#)) // typo for nillable
#expect(rejects(#""#))
#expect(rejects(#""#)) // sequence has no name
// A foreign-namespace attribute is allowed.
try? compile(#""#)
}
@Test("ref excludes name and type")
func test_refExclusions() {
#expect(rejects(#""#))
#expect(rejects(#""#))
}
@Test("element declarations in one content model must be consistent")
func test_elementDeclsConsistent() throws {
// Same name, different type in one content model is invalid.
#expect(rejects(#"""
"""#))
// Same name, same type is consistent (separated so it stays unambiguous).
try compile(#"""
"""#)
// A same name in a NESTED complex type is a different content model: allowed.
try compile(#"""
"""#)
}
@Test("an identity constraint requires a selector and field")
func test_identityConstraintRequiresParts() {
#expect(rejects(#"""
"""#))
}
@Test("a complexContent derivation's content must be ordered: model group, attributes, anyAttribute")
func test_complexContentDerivationOrder() {
func derive(_ kind: String, _ body: String) -> String {
#"""
\#(body)
"""#
}
// Two model groups (ctG/ctH "X then Y").
#expect(rejects(derive("restriction", #""#)))
#expect(rejects(derive("extension", #""#)))
// A model group after an attribute.
#expect(rejects(derive("restriction", #""#)))
// An attribute after anyAttribute, and two anyAttributes.
#expect(rejects(derive("extension", #""#)))
#expect(rejects(derive("restriction", #""#)))
}
@Test("a well-ordered complexContent derivation compiles")
func test_complexContentDerivationValid() throws {
try compile(#"""
"""#)
// No model group is fine (an extension that only adds attributes).
try compile(#"""
"""#)
}
@Test("a complexType's shorthand content must be ordered: model group, attributes, anyAttribute")
func test_complexTypeShorthandOrder() {
// A model group after an attribute, two anyAttributes, an attribute after anyAttribute (ctB).
#expect(rejects(#""#))
#expect(rejects(#""#))
#expect(rejects(#""#))
// A well-ordered shorthand complexType compiles.
#expect(!rejects(#"""
"""#))
}
}