Merge epic/10-xsd-parser: XSD schema-document parser and end-to-end validation
This commit is contained in:
commit
e845b63ff0
@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- XSD schema-document parser, completing the XSD validation pipeline (toward #2).
|
||||
`PureXML.Schema.Document(xsd)` compiles a schema document into its global
|
||||
element declarations and named-type table, and `validate(_:)` checks an
|
||||
instance document end to end. The parser matches the schema vocabulary by local
|
||||
name and supports global and local element declarations, named and inline
|
||||
simple and complex types, `sequence`/`choice`/`all` model groups with
|
||||
occurrence, attribute uses, simple content (restriction/extension), the full
|
||||
facet set, and recursive types (via named type references resolved during
|
||||
validation).
|
||||
- XSD complex types and particles (toward #2 schema validation).
|
||||
`PureXML.Schema.ComplexValidator` validates an element against a
|
||||
`ComplexType`: its attribute uses (required and typed, with unknown-attribute
|
||||
|
||||
@ -48,10 +48,13 @@ public extension PureXML.Schema {
|
||||
}
|
||||
}
|
||||
|
||||
/// The declared type of an element: a simple type (text only) or a complex one.
|
||||
/// The declared type of an element: a simple type (text only), a complex one,
|
||||
/// or a named reference resolved against the schema's type table (which lets
|
||||
/// recursive schemas be represented without an infinite value).
|
||||
indirect enum ElementType: Sendable {
|
||||
case simple(SimpleType)
|
||||
case complex(ComplexType)
|
||||
case typeReference(String)
|
||||
}
|
||||
|
||||
/// An attribute use on a complex type.
|
||||
|
||||
@ -3,7 +3,12 @@ public extension PureXML.Schema {
|
||||
/// content model, and, recursively, each child element against the type
|
||||
/// declared for its name.
|
||||
struct ComplexValidator {
|
||||
public init() {}
|
||||
/// The named type table that `ElementType.typeReference` resolves against.
|
||||
private let types: [String: ElementType]
|
||||
|
||||
public init(types: [String: ElementType] = [:]) {
|
||||
self.types = types
|
||||
}
|
||||
|
||||
/// Validates `element` against `type`, returning one issue per violation.
|
||||
public func validate(
|
||||
@ -16,6 +21,17 @@ public extension PureXML.Schema {
|
||||
return issues
|
||||
}
|
||||
|
||||
/// Validates `element` against any element type, resolving a
|
||||
/// `typeReference` through the type table.
|
||||
public func validate(
|
||||
_ element: PureXML.Model.Element,
|
||||
as type: ElementType,
|
||||
) -> [PureXML.Validation.Issue] {
|
||||
var issues: [PureXML.Validation.Issue] = []
|
||||
validateChild(element, against: type, into: &issues)
|
||||
return issues
|
||||
}
|
||||
|
||||
// MARK: Attributes
|
||||
|
||||
private func validateAttributes(
|
||||
@ -91,27 +107,41 @@ public extension PureXML.Schema {
|
||||
issues.append(.init(severity: .error, message: "content does not match the content model"))
|
||||
return
|
||||
}
|
||||
validateChildren(children, types: Self.elementTypes(in: particle.term), into: &issues)
|
||||
validateChildren(children, childTypes: Self.elementTypes(in: particle.term), into: &issues)
|
||||
}
|
||||
|
||||
private func validateChildren(
|
||||
_ children: [PureXML.Model.Element],
|
||||
types: [String: ElementType],
|
||||
childTypes: [String: ElementType],
|
||||
into issues: inout [PureXML.Validation.Issue],
|
||||
) {
|
||||
for child in children {
|
||||
guard let type = types[Self.key(child.name)] else { continue }
|
||||
switch type {
|
||||
case let .simple(simple):
|
||||
if !child.children.compactMap(\.element).isEmpty {
|
||||
issues.append(.init(severity: .error, message: "'\(child.name.localName)' must not have children"))
|
||||
}
|
||||
if let error = simple.validate(Self.textContent(child)) {
|
||||
issues.append(.init(severity: .error, message: "'\(child.name.localName)': \(error)"))
|
||||
}
|
||||
case let .complex(complex):
|
||||
issues.append(contentsOf: validate(child, against: complex))
|
||||
guard let declared = childTypes[Self.key(child.name)] else { continue }
|
||||
validateChild(child, against: declared, into: &issues)
|
||||
}
|
||||
}
|
||||
|
||||
private func validateChild(
|
||||
_ child: PureXML.Model.Element,
|
||||
against declared: ElementType,
|
||||
into issues: inout [PureXML.Validation.Issue],
|
||||
) {
|
||||
switch declared {
|
||||
case let .simple(simple):
|
||||
if !child.children.compactMap(\.element).isEmpty {
|
||||
issues.append(.init(severity: .error, message: "'\(child.name.localName)' must not have children"))
|
||||
}
|
||||
if let error = simple.validate(Self.textContent(child)) {
|
||||
issues.append(.init(severity: .error, message: "'\(child.name.localName)': \(error)"))
|
||||
}
|
||||
case let .complex(complex):
|
||||
issues.append(contentsOf: validate(child, against: complex))
|
||||
case let .typeReference(name):
|
||||
guard let resolved = types[name] else {
|
||||
issues.append(.init(severity: .error, message: "unknown type '\(name)'"))
|
||||
return
|
||||
}
|
||||
validateChild(child, against: resolved, into: &issues)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
39
Sources/Schema/SchemaDocument.swift
Normal file
39
Sources/Schema/SchemaDocument.swift
Normal file
@ -0,0 +1,39 @@
|
||||
public extension PureXML.Schema {
|
||||
/// An error compiling or applying a schema.
|
||||
enum SchemaError: Swift.Error, Equatable, Sendable, CustomStringConvertible {
|
||||
case notASchema
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .notASchema: "the document is not an xs:schema"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A compiled XSD schema: its global element declarations and named-type
|
||||
/// table, parsed from a schema document, used to validate instance documents.
|
||||
struct Document: Sendable {
|
||||
private let elements: [String: ElementType]
|
||||
private let types: [String: ElementType]
|
||||
|
||||
/// Compiles a schema document.
|
||||
public init(_ xsd: String) throws {
|
||||
(elements, types) = try XSDParser.parse(xsd)
|
||||
}
|
||||
|
||||
/// Validates an instance document against the schema, returning one issue
|
||||
/// per violation. Reports an issue when the root element has no global
|
||||
/// declaration.
|
||||
public func validate(_ xml: String) throws -> [PureXML.Validation.Issue] {
|
||||
guard case let .document(children) = try PureXML.parse(xml),
|
||||
let root = children.compactMap(\.element).first
|
||||
else {
|
||||
return [.init(severity: .error, message: "the document has no root element")]
|
||||
}
|
||||
guard let declaration = elements[root.name.localName] else {
|
||||
return [.init(severity: .error, message: "no element declaration for '\(root.name.localName)'")]
|
||||
}
|
||||
return ComplexValidator(types: types).validate(root, as: declaration)
|
||||
}
|
||||
}
|
||||
}
|
||||
266
Sources/Schema/XSDParser.swift
Normal file
266
Sources/Schema/XSDParser.swift
Normal file
@ -0,0 +1,266 @@
|
||||
private typealias Tree = PureXML.Model.TreeNode
|
||||
|
||||
/// Tree-access helpers shared by the schema parser. File-scope and private.
|
||||
private enum XSDNode {
|
||||
static func localName(_ node: Tree) -> String? {
|
||||
node.name?.localName
|
||||
}
|
||||
|
||||
static func attribute(_ node: Tree, _ name: String) -> String? {
|
||||
node.attributes.first { $0.name.localName == name }?.value
|
||||
}
|
||||
|
||||
static func elementChildren(_ node: Tree) -> [Tree] {
|
||||
node.children.filter { $0.kind == .element }
|
||||
}
|
||||
|
||||
static func children(_ node: Tree, named name: String) -> [Tree] {
|
||||
elementChildren(node).filter { localName($0) == name }
|
||||
}
|
||||
|
||||
static func firstChild(_ node: Tree, named name: String) -> Tree? {
|
||||
children(node, named: name).first
|
||||
}
|
||||
|
||||
static func stripPrefix(_ qualified: String) -> String {
|
||||
qualified.split(separator: ":").last.map(String.init) ?? qualified
|
||||
}
|
||||
|
||||
static func occurrence(_ node: Tree) -> (min: Int, max: Int?) {
|
||||
let minimum = attribute(node, "minOccurs").flatMap(Int.init) ?? 1
|
||||
let maximumText = attribute(node, "maxOccurs")
|
||||
let maximum: Int? = maximumText == "unbounded" ? nil : (maximumText.flatMap(Int.init) ?? 1)
|
||||
return (minimum, maximum)
|
||||
}
|
||||
}
|
||||
|
||||
extension PureXML.Schema {
|
||||
/// Parses an XSD schema document into its global element declarations and its
|
||||
/// named-type table. Matches the schema vocabulary by local name, so the XML
|
||||
/// Schema namespace prefix may be anything. Supports global and local element
|
||||
/// declarations, named and inline simple and complex types, `sequence`,
|
||||
/// `choice`, and `all` model groups with occurrence, attribute uses, simple
|
||||
/// content, and the full facet set.
|
||||
enum XSDParser {
|
||||
static func parse(
|
||||
_ xsd: String,
|
||||
) throws -> (elements: [String: ElementType], types: [String: ElementType]) {
|
||||
let root = try PureXML.parseTree(xsd)
|
||||
guard let schema = XSDNode.elementChildren(root).first(where: { XSDNode.localName($0) == "schema" }) else {
|
||||
throw PureXML.Schema.SchemaError.notASchema
|
||||
}
|
||||
var simpleTypes: [String: SimpleType] = [:]
|
||||
for node in XSDNode.children(schema, named: "simpleType") {
|
||||
if let name = XSDNode.attribute(node, "name") {
|
||||
simpleTypes[name] = simpleType(node, simpleTypes)
|
||||
}
|
||||
}
|
||||
var types: [String: ElementType] = simpleTypes.mapValues(ElementType.simple)
|
||||
for node in XSDNode.children(schema, named: "complexType") {
|
||||
if let name = XSDNode.attribute(node, "name") {
|
||||
types[name] = .complex(complexType(node, simpleTypes))
|
||||
}
|
||||
}
|
||||
var elements: [String: ElementType] = [:]
|
||||
for node in XSDNode.children(schema, named: "element") {
|
||||
if let name = XSDNode.attribute(node, "name") {
|
||||
elements[name] = elementType(node, simpleTypes)
|
||||
}
|
||||
}
|
||||
return (elements, types)
|
||||
}
|
||||
|
||||
// MARK: Element and type references
|
||||
|
||||
private static func elementType(_ node: Tree, _ simpleTypes: [String: SimpleType]) -> ElementType {
|
||||
if let typeName = XSDNode.attribute(node, "type") {
|
||||
return typeReference(typeName)
|
||||
}
|
||||
if let inline = XSDNode.firstChild(node, named: "simpleType") {
|
||||
return .simple(simpleType(inline, simpleTypes))
|
||||
}
|
||||
if let inline = XSDNode.firstChild(node, named: "complexType") {
|
||||
return .complex(complexType(inline, simpleTypes))
|
||||
}
|
||||
return .complex(anyType)
|
||||
}
|
||||
|
||||
private static func typeReference(_ typeName: String) -> ElementType {
|
||||
let local = XSDNode.stripPrefix(typeName)
|
||||
if let builtin = BuiltinType(rawValue: local) {
|
||||
return .simple(SimpleType(base: builtin))
|
||||
}
|
||||
return .typeReference(local)
|
||||
}
|
||||
|
||||
private static func simpleTypeReference(_ typeName: String, _ simpleTypes: [String: SimpleType]) -> SimpleType {
|
||||
let local = XSDNode.stripPrefix(typeName)
|
||||
if let builtin = BuiltinType(rawValue: local) { return SimpleType(base: builtin) }
|
||||
return simpleTypes[local] ?? SimpleType(base: .string)
|
||||
}
|
||||
|
||||
private static var anyType: ComplexType {
|
||||
ComplexType(
|
||||
allowsOtherAttributes: true,
|
||||
content: .mixed(Particle(minOccurs: 0, maxOccurs: nil, term: .wildcard(Wildcard()))),
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: Simple types and facets
|
||||
|
||||
private static func simpleType(_ node: Tree, _ simpleTypes: [String: SimpleType]) -> SimpleType {
|
||||
guard let restriction = XSDNode.firstChild(node, named: "restriction") else {
|
||||
return SimpleType(base: .string)
|
||||
}
|
||||
let baseName = XSDNode.stripPrefix(XSDNode.attribute(restriction, "base") ?? "string")
|
||||
var base = BuiltinType.string
|
||||
var facets = Facets()
|
||||
if let builtin = BuiltinType(rawValue: baseName) {
|
||||
base = builtin
|
||||
} else if let parent = simpleTypes[baseName] {
|
||||
base = parent.base
|
||||
facets = parent.facets
|
||||
}
|
||||
applyFacets(restriction, into: &facets)
|
||||
return SimpleType(base: base, facets: facets)
|
||||
}
|
||||
|
||||
private static func applyFacets(_ restriction: Tree, into facets: inout Facets) {
|
||||
for facet in XSDNode.elementChildren(restriction) {
|
||||
let value = XSDNode.attribute(facet, "value")
|
||||
applyStringFacet(XSDNode.localName(facet), value, into: &facets)
|
||||
applyNumericFacet(XSDNode.localName(facet), value, into: &facets)
|
||||
}
|
||||
}
|
||||
|
||||
private static func applyStringFacet(_ name: String?, _ value: String?, into facets: inout Facets) {
|
||||
switch name {
|
||||
case "pattern": if let value { facets.patterns.append(value) }
|
||||
case "enumeration": if let value { facets.enumeration = (facets.enumeration ?? []) + [value] }
|
||||
case "minInclusive": facets.minInclusive = value
|
||||
case "maxInclusive": facets.maxInclusive = value
|
||||
case "minExclusive": facets.minExclusive = value
|
||||
case "maxExclusive": facets.maxExclusive = value
|
||||
case "whiteSpace": facets.whiteSpace = whiteSpace(value)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
private static func applyNumericFacet(_ name: String?, _ value: String?, into facets: inout Facets) {
|
||||
let number = value.flatMap(Int.init)
|
||||
switch name {
|
||||
case "length": facets.length = number
|
||||
case "minLength": facets.minLength = number
|
||||
case "maxLength": facets.maxLength = number
|
||||
case "totalDigits": facets.totalDigits = number
|
||||
case "fractionDigits": facets.fractionDigits = number
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
private static func whiteSpace(_ value: String?) -> WhiteSpace? {
|
||||
switch value {
|
||||
case "preserve": .preserve
|
||||
case "replace": .replace
|
||||
case "collapse": .collapse
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Complex types
|
||||
|
||||
private static func complexType(_ node: Tree, _ simpleTypes: [String: SimpleType]) -> ComplexType {
|
||||
let mixed = XSDNode.attribute(node, "mixed") == "true"
|
||||
var attributes = attributeUses(under: node, simpleTypes)
|
||||
if let simpleContent = XSDNode.firstChild(node, named: "simpleContent") {
|
||||
let inner = derivation(simpleContent)
|
||||
attributes += inner.map { attributeUses(under: $0, simpleTypes) } ?? []
|
||||
return ComplexType(attributes: attributes, content: .simpleContent(simpleContentType(simpleContent, simpleTypes)))
|
||||
}
|
||||
let container = XSDNode.firstChild(node, named: "complexContent").flatMap(derivation) ?? node
|
||||
attributes += container === node ? [] : attributeUses(under: container, simpleTypes)
|
||||
guard let particle = modelGroup(in: container, simpleTypes) else {
|
||||
return ComplexType(attributes: attributes, content: .empty)
|
||||
}
|
||||
return ComplexType(attributes: attributes, content: mixed ? .mixed(particle) : .elementOnly(particle))
|
||||
}
|
||||
|
||||
private static func derivation(_ node: Tree) -> Tree? {
|
||||
XSDNode.firstChild(node, named: "restriction") ?? XSDNode.firstChild(node, named: "extension")
|
||||
}
|
||||
|
||||
private static func simpleContentType(_ node: Tree, _ simpleTypes: [String: SimpleType]) -> SimpleType {
|
||||
guard let inner = derivation(node) else { return SimpleType(base: .string) }
|
||||
let baseName = XSDNode.stripPrefix(XSDNode.attribute(inner, "base") ?? "string")
|
||||
let base = BuiltinType(rawValue: baseName) ?? simpleTypes[baseName]?.base ?? .string
|
||||
var facets = simpleTypes[baseName]?.facets ?? Facets()
|
||||
applyFacets(inner, into: &facets)
|
||||
return SimpleType(base: base, facets: facets)
|
||||
}
|
||||
|
||||
private static func attributeUses(under node: Tree, _ simpleTypes: [String: SimpleType]) -> [AttributeUse] {
|
||||
XSDNode.children(node, named: "attribute").compactMap { attributeUse($0, simpleTypes) }
|
||||
}
|
||||
|
||||
private static func attributeUse(_ node: Tree, _ simpleTypes: [String: SimpleType]) -> AttributeUse? {
|
||||
guard let name = XSDNode.attribute(node, "name") else { return nil }
|
||||
let required = XSDNode.attribute(node, "use") == "required"
|
||||
let type: SimpleType = if let typeName = XSDNode.attribute(node, "type") {
|
||||
simpleTypeReference(typeName, simpleTypes)
|
||||
} else if let inline = XSDNode.firstChild(node, named: "simpleType") {
|
||||
simpleType(inline, simpleTypes)
|
||||
} else {
|
||||
SimpleType(base: .string)
|
||||
}
|
||||
return AttributeUse(name: PureXML.Model.QualifiedName(name), type: type, required: required)
|
||||
}
|
||||
|
||||
// MARK: Model groups
|
||||
|
||||
private static func modelGroup(in node: Tree, _ simpleTypes: [String: SimpleType]) -> Particle? {
|
||||
for (name, compositor) in [("sequence", Compositor.sequence), ("choice", .choice), ("all", .all)] {
|
||||
if let group = XSDNode.firstChild(node, named: name) {
|
||||
return groupParticle(group, compositor, simpleTypes)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func groupParticle(
|
||||
_ node: Tree,
|
||||
_ compositor: Compositor,
|
||||
_ simpleTypes: [String: SimpleType],
|
||||
) -> Particle {
|
||||
var particles: [Particle] = []
|
||||
for child in XSDNode.elementChildren(node) {
|
||||
if let member = particle(child, simpleTypes) { particles.append(member) }
|
||||
}
|
||||
let (minimum, maximum) = XSDNode.occurrence(node)
|
||||
return Particle(
|
||||
minOccurs: minimum,
|
||||
maxOccurs: maximum,
|
||||
term: .group(Group(compositor: compositor, particles: particles)),
|
||||
)
|
||||
}
|
||||
|
||||
private static func particle(_ node: Tree, _ simpleTypes: [String: SimpleType]) -> Particle? {
|
||||
let (minimum, maximum) = XSDNode.occurrence(node)
|
||||
switch XSDNode.localName(node) {
|
||||
case "element":
|
||||
let name = XSDNode.attribute(node, "name") ?? XSDNode.stripPrefix(XSDNode.attribute(node, "ref") ?? "")
|
||||
let type = XSDNode.attribute(node, "name") != nil ? elementType(node, simpleTypes) : ElementType.complex(anyType)
|
||||
return Particle(minOccurs: minimum, maxOccurs: maximum, term: .element(name: PureXML.Model.QualifiedName(name), type: type))
|
||||
case "sequence":
|
||||
return groupParticle(node, .sequence, simpleTypes)
|
||||
case "choice":
|
||||
return groupParticle(node, .choice, simpleTypes)
|
||||
case "all":
|
||||
return groupParticle(node, .all, simpleTypes)
|
||||
case "any":
|
||||
return Particle(minOccurs: minimum, maxOccurs: maximum, term: .wildcard(Wildcard()))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
124
Tests/XSDSchemaTests.swift
Normal file
124
Tests/XSDSchemaTests.swift
Normal file
@ -0,0 +1,124 @@
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
@Suite("XSD schema documents")
|
||||
struct XSDSchemaTests {
|
||||
private func validate(_ xsd: String, _ xml: String) throws -> [PureXML.Validation.Issue] {
|
||||
try PureXML.Schema.Document(xsd).validate(xml)
|
||||
}
|
||||
|
||||
@Test("A simple element with a built-in type")
|
||||
func test_simpleElement() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="age" type="xs:nonNegativeInteger"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(try validate(xsd, "<age>30</age>").isEmpty)
|
||||
#expect(try !validate(xsd, "<age>-5</age>").isEmpty)
|
||||
#expect(try !validate(xsd, "<age>x</age>").isEmpty)
|
||||
}
|
||||
|
||||
@Test("A named simpleType with facets")
|
||||
func test_namedSimpleType() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:simpleType name="sku">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:pattern value="[A-Z]{3}-\\d{4}"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
<xs:element name="code" type="sku"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(try validate(xsd, "<code>ABC-1234</code>").isEmpty)
|
||||
#expect(try !validate(xsd, "<code>abc-1234</code>").isEmpty)
|
||||
}
|
||||
|
||||
@Test("A complex type with a sequence and attributes")
|
||||
func test_complexType() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="book">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="title" type="xs:string"/>
|
||||
<xs:element name="year" type="xs:gYear" minOccurs="0"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="isbn" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(try validate(xsd, "<book isbn=\"1\"><title>T</title><year>2026</year></book>").isEmpty)
|
||||
#expect(try validate(xsd, "<book isbn=\"1\"><title>T</title></book>").isEmpty)
|
||||
#expect(try !validate(xsd, "<book><title>T</title></book>").isEmpty) // missing isbn
|
||||
#expect(try !validate(xsd, "<book isbn=\"1\"><year>2026</year></book>").isEmpty) // missing title
|
||||
#expect(try !validate(xsd, "<book isbn=\"1\"><title>T</title><year>nope</year></book>").isEmpty)
|
||||
}
|
||||
|
||||
@Test("A named complex type referenced by elements")
|
||||
func test_namedComplexType() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:complexType name="point">
|
||||
<xs:sequence>
|
||||
<xs:element name="x" type="xs:decimal"/>
|
||||
<xs:element name="y" type="xs:decimal"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
<xs:element name="location" type="point"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(try validate(xsd, "<location><x>1.5</x><y>2.5</y></location>").isEmpty)
|
||||
#expect(try !validate(xsd, "<location><x>1.5</x></location>").isEmpty)
|
||||
}
|
||||
|
||||
@Test("Recursive types validate to any depth")
|
||||
func test_recursiveType() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:complexType name="node">
|
||||
<xs:sequence>
|
||||
<xs:element name="value" type="xs:int"/>
|
||||
<xs:element name="child" type="node" minOccurs="0"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
<xs:element name="tree" type="node"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
let nested = "<tree><value>1</value><child><value>2</value><child><value>3</value></child></child></tree>"
|
||||
#expect(try validate(xsd, nested).isEmpty)
|
||||
let bad = "<tree><value>1</value><child><value>x</value></child></tree>"
|
||||
#expect(try !validate(xsd, bad).isEmpty)
|
||||
}
|
||||
|
||||
@Test("Simple content restricts the text and keeps attributes")
|
||||
func test_simpleContent() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="price">
|
||||
<xs:complexType>
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="xs:decimal">
|
||||
<xs:attribute name="currency" type="xs:string"/>
|
||||
</xs:extension>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(try validate(xsd, "<price currency=\"USD\">9.99</price>").isEmpty)
|
||||
#expect(try !validate(xsd, "<price>nope</price>").isEmpty)
|
||||
}
|
||||
|
||||
@Test("An unknown root element is reported")
|
||||
func test_unknownRoot() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="known" type="xs:string"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(try !validate(xsd, "<unknown/>").isEmpty)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user