diff --git a/Sources/Schema/SimpleType.swift b/Sources/Schema/SimpleType.swift index e499351..464f4d3 100644 --- a/Sources/Schema/SimpleType.swift +++ b/Sources/Schema/SimpleType.swift @@ -44,16 +44,38 @@ public extension PureXML.Schema { } } - /// A simple type: a built-in base restricted by facets. Validates a lexical - /// value against the XSD Part 2 rules: whitespace processing, the base value - /// space, the base's intrinsic bounds, then the facets. + /// A simple type's variety (XSD Part 2): atomic, a whitespace-separated list + /// of an item type, or a union of member types. + indirect enum Variety: Sendable { + case atomic + case list(SimpleType) + case union([SimpleType]) + } + + /// A simple type: a built-in base restricted by facets, of one of the three + /// varieties. Validates a lexical value against the XSD Part 2 rules: + /// whitespace processing, the base value space, the base's intrinsic bounds, + /// then the facets. A list checks each item against the item type and counts + /// items for the length facets; a union admits a value valid for any member. struct SimpleType: Sendable { public var base: BuiltinType public var facets: Facets + public var variety: Variety - public init(base: BuiltinType, facets: Facets = Facets()) { + public init(base: BuiltinType, facets: Facets = Facets(), variety: Variety = .atomic) { self.base = base self.facets = facets + self.variety = variety + } + + /// A list type whose items are `item`, carrying its own (list) facets. + public static func list(item: SimpleType, facets: Facets = Facets()) -> SimpleType { + SimpleType(base: .string, facets: facets, variety: .list(item)) + } + + /// A union of `members`: a value is valid if any member admits it. + public static func union(_ members: [SimpleType]) -> SimpleType { + SimpleType(base: .string, variety: .union(members)) } /// Whether `lexical` is a valid value of this type. @@ -64,6 +86,37 @@ public extension PureXML.Schema { /// A description of the first constraint `lexical` violates, or nil when it /// is valid. public func validate(_ lexical: String) -> String? { + switch variety { + case .atomic: validateAtomic(lexical) + case let .list(item): validateList(lexical, item: item) + case let .union(members): validateUnion(lexical, members: members) + } + } + + private func validateList(_ lexical: String, item: SimpleType) -> String? { + let normalized = Self.process(lexical, whiteSpace: facets.whiteSpace ?? .collapse) + let tokens = normalized.isEmpty ? [] : normalized.split(separator: " ").map(String.init) + for token in tokens { + if let error = item.validate(token) { return error } + } + if let exact = facets.length, tokens.count != exact { return "list length \(tokens.count) is not \(exact)" } + if let minimum = facets.minLength, tokens.count < minimum { return "list length \(tokens.count) is below \(minimum)" } + if let maximum = facets.maxLength, tokens.count > maximum { return "list length \(tokens.count) is above \(maximum)" } + if let error = patternError(normalized) { return error } + if let enumeration = facets.enumeration, !enumeration.contains(normalized) { + return "'\(normalized)' is not in the enumeration" + } + return nil + } + + private func validateUnion(_ lexical: String, members: [SimpleType]) -> String? { + for member in members where member.validate(lexical) == nil { + return nil + } + return "'\(lexical)' does not match any member type of the union" + } + + private func validateAtomic(_ lexical: String) -> String? { let value = Self.process(lexical, whiteSpace: facets.whiteSpace ?? base.whiteSpace) let primitive = base.primitive guard primitive.isValid(value) else { diff --git a/Sources/Schema/XSDParser.swift b/Sources/Schema/XSDParser.swift index b2942fa..b6cc5e5 100644 --- a/Sources/Schema/XSDParser.swift +++ b/Sources/Schema/XSDParser.swift @@ -34,13 +34,31 @@ private enum XSDNode { } } +/// The parsing context: the named simple types resolved so far, plus the +/// definition nodes for named attribute groups and model groups (so their refs +/// can be expanded), and a guard against cyclic group references. File-scope and +/// private. +private struct Context { + var simpleTypes: [String: PureXML.Schema.SimpleType] + var attributeGroups: [String: Tree] + var groups: [String: Tree] + var visitingGroups: Set = [] + + func visiting(_ group: String) -> Context { + var copy = self + copy.visitingGroups.insert(group) + return copy + } +} + 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. + /// content, the full facet set, `list` and `union` simple types, named + /// attribute groups and model groups, and element and group references. enum XSDParser { static func parse( _ xsd: String, @@ -49,38 +67,67 @@ extension PureXML.Schema { 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 context = Context( + simpleTypes: [:], + attributeGroups: indexByName(XSDNode.children(schema, named: "attributeGroup")), + groups: indexByName(XSDNode.children(schema, named: "group")), + ) + // Named simple types may restrict or list one another regardless of + // document order, so resolve them over repeated passes: each pass sees + // the types resolved by the previous one. A chain of length n settles + // in at most n passes. + let simpleNodes = XSDNode.children(schema, named: "simpleType").filter { XSDNode.attribute($0, "name") != nil } + for _ in simpleNodes.indices { + for node in simpleNodes { + if let name = XSDNode.attribute(node, "name") { + context.simpleTypes[name] = XSDSimpleParser.simpleType(node, context) + } } } - var types: [String: ElementType] = simpleTypes.mapValues(ElementType.simple) + var types: [String: ElementType] = context.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)) + types[name] = .complex(complexType(node, context)) } } var elements: [String: ElementType] = [:] for node in XSDNode.children(schema, named: "element") { if let name = XSDNode.attribute(node, "name") { - elements[name] = elementType(node, simpleTypes) + let type = elementType(node, context) + elements[name] = type + // An `xs:element ref="name"` resolves to this declaration's type + // through a reserved key (a colon keeps it out of the NCName-only + // type namespace), letting forward and recursive refs resolve at + // validation time. + types[elementKey(name)] = type } } return (elements, types) } + static func elementKey(_ name: String) -> String { + "element:\(name)" + } + + private static func indexByName(_ nodes: [Tree]) -> [String: Tree] { + var index: [String: Tree] = [:] + for node in nodes { + if let name = XSDNode.attribute(node, "name") { index[name] = node } + } + return index + } + // MARK: Element and type references - private static func elementType(_ node: Tree, _ simpleTypes: [String: SimpleType]) -> ElementType { + private static func elementType(_ node: Tree, _ context: Context) -> ElementType { if let typeName = XSDNode.attribute(node, "type") { return typeReference(typeName) } if let inline = XSDNode.firstChild(node, named: "simpleType") { - return .simple(simpleType(inline, simpleTypes)) + return .simple(XSDSimpleParser.simpleType(inline, context)) } if let inline = XSDNode.firstChild(node, named: "complexType") { - return .complex(complexType(inline, simpleTypes)) + return .complex(complexType(inline, context)) } return .complex(anyType) } @@ -93,12 +140,6 @@ extension PureXML.Schema { 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, @@ -106,26 +147,186 @@ extension PureXML.Schema { ) } - // MARK: Simple types and facets + // MARK: Complex types - private static func simpleType(_ node: Tree, _ simpleTypes: [String: SimpleType]) -> SimpleType { + private static func complexType(_ node: Tree, _ context: Context) -> ComplexType { + let mixed = XSDNode.attribute(node, "mixed") == "true" + var attributes = attributeUses(under: node, context) + if let simpleContent = XSDNode.firstChild(node, named: "simpleContent") { + let inner = derivation(simpleContent) + attributes += inner.map { attributeUses(under: $0, context) } ?? [] + return ComplexType(attributes: attributes, content: .simpleContent(simpleContentType(simpleContent, context))) + } + let container = XSDNode.firstChild(node, named: "complexContent").flatMap(derivation) ?? node + attributes += container === node ? [] : attributeUses(under: container, context) + guard let particle = modelGroup(in: container, context) 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, _ context: Context) -> 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) ?? context.simpleTypes[baseName]?.base ?? .string + var facets = context.simpleTypes[baseName]?.facets ?? Facets() + XSDSimpleParser.applyFacets(inner, into: &facets) + return SimpleType(base: base, facets: facets) + } + + // MARK: Attribute uses and groups + + private static func attributeUses(under node: Tree, _ context: Context, visited: Set = []) -> [AttributeUse] { + var uses: [AttributeUse] = [] + for child in XSDNode.elementChildren(node) { + switch XSDNode.localName(child) { + case "attribute": + if let use = attributeUse(child, context) { uses.append(use) } + case "attributeGroup": + guard let ref = XSDNode.attribute(child, "ref") else { break } + let name = XSDNode.stripPrefix(ref) + guard !visited.contains(name), let group = context.attributeGroups[name] else { break } + uses += attributeUses(under: group, context, visited: visited.union([name])) + default: + break + } + } + return uses + } + + private static func attributeUse(_ node: Tree, _ context: Context) -> 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") { + XSDSimpleParser.simpleTypeReference(typeName, context) + } else if let inline = XSDNode.firstChild(node, named: "simpleType") { + XSDSimpleParser.simpleType(inline, context) + } 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, _ context: Context) -> Particle? { + for (name, compositor) in [("sequence", Compositor.sequence), ("choice", .choice), ("all", .all)] { + if let group = XSDNode.firstChild(node, named: name) { + return groupParticle(group, compositor, context) + } + } + if let groupRef = XSDNode.firstChild(node, named: "group") { + return particle(groupRef, context) + } + return nil + } + + private static func groupParticle( + _ node: Tree, + _ compositor: Compositor, + _ context: Context, + ) -> Particle { + var particles: [Particle] = [] + for child in XSDNode.elementChildren(node) { + if let member = particle(child, context) { 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, _ context: Context) -> Particle? { + let (minimum, maximum) = XSDNode.occurrence(node) + switch XSDNode.localName(node) { + case "element": + return elementParticle(node, minimum, maximum, context) + case "sequence": + return groupParticle(node, .sequence, context) + case "choice": + return groupParticle(node, .choice, context) + case "all": + return groupParticle(node, .all, context) + case "group": + return groupReferenceParticle(node, minimum, maximum, context) + case "any": + return Particle(minOccurs: minimum, maxOccurs: maximum, term: .wildcard(Wildcard())) + default: + return nil + } + } + + private static func elementParticle(_ node: Tree, _ minimum: Int, _ maximum: Int?, _ context: Context) -> Particle { + if let ref = XSDNode.attribute(node, "ref") { + let name = XSDNode.stripPrefix(ref) + return Particle( + minOccurs: minimum, + maxOccurs: maximum, + term: .element(name: PureXML.Model.QualifiedName(name), type: .typeReference(elementKey(name))), + ) + } + let name = XSDNode.attribute(node, "name") ?? "" + return Particle( + minOccurs: minimum, + maxOccurs: maximum, + term: .element(name: PureXML.Model.QualifiedName(name), type: elementType(node, context)), + ) + } + + private static func groupReferenceParticle(_ node: Tree, _ minimum: Int, _ maximum: Int?, _ context: Context) -> Particle? { + guard let ref = XSDNode.attribute(node, "ref") else { return nil } + let name = XSDNode.stripPrefix(ref) + guard !context.visitingGroups.contains(name), let definition = context.groups[name], + let inner = modelGroup(in: definition, context.visiting(name)) + else { + return nil + } + return Particle(minOccurs: minimum, maxOccurs: maximum, term: inner.term) + } + } + + /// Parses XSD simple types: atomic restrictions with the full facet set, and + /// the `list` and `union` varieties. Kept beside ``XSDParser`` so the two + /// share the file-scope tree helpers and parsing context. + enum XSDSimpleParser { + fileprivate static func simpleType(_ node: Tree, _ context: Context) -> SimpleType { + if let list = XSDNode.firstChild(node, named: "list") { + return listType(list, context) + } + if let union = XSDNode.firstChild(node, named: "union") { + return unionType(union, context) + } 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() + var variety = Variety.atomic if let builtin = BuiltinType(rawValue: baseName) { base = builtin - } else if let parent = simpleTypes[baseName] { + } else if let parent = context.simpleTypes[baseName] { base = parent.base facets = parent.facets + variety = parent.variety } applyFacets(restriction, into: &facets) - return SimpleType(base: base, facets: facets) + return SimpleType(base: base, facets: facets, variety: variety) } - private static func applyFacets(_ restriction: Tree, into facets: inout Facets) { + fileprivate static func simpleTypeReference(_ typeName: String, _ context: Context) -> SimpleType { + let local = XSDNode.stripPrefix(typeName) + if let builtin = BuiltinType(rawValue: local) { return SimpleType(base: builtin) } + return context.simpleTypes[local] ?? SimpleType(base: .string) + } + + fileprivate 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) @@ -133,6 +334,26 @@ extension PureXML.Schema { } } + private static func listType(_ node: Tree, _ context: Context) -> SimpleType { + let item: SimpleType = if let itemType = XSDNode.attribute(node, "itemType") { + simpleTypeReference(itemType, context) + } else if let inline = XSDNode.firstChild(node, named: "simpleType") { + simpleType(inline, context) + } else { + SimpleType(base: .string) + } + return .list(item: item) + } + + private static func unionType(_ node: Tree, _ context: Context) -> SimpleType { + var members: [SimpleType] = [] + if let names = XSDNode.attribute(node, "memberTypes") { + members += names.split(whereSeparator: \.isWhitespace).map { simpleTypeReference(String($0), context) } + } + members += XSDNode.children(node, named: "simpleType").map { simpleType($0, context) } + return .union(members) + } + private static func applyStringFacet(_ name: String?, _ value: String?, into facets: inout Facets) { switch name { case "pattern": if let value { facets.patterns.append(value) } @@ -166,101 +387,5 @@ extension PureXML.Schema { 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 - } - } } } diff --git a/Tests/XSDCompositionTests.swift b/Tests/XSDCompositionTests.swift new file mode 100644 index 0000000..cd8c747 --- /dev/null +++ b/Tests/XSDCompositionTests.swift @@ -0,0 +1,124 @@ +@testable import PureXML +import Testing + +@Suite("XSD list, union, groups, and references") +struct XSDCompositionTests { + private func validate(_ xsd: String, _ xml: String) throws -> [PureXML.Validation.Issue] { + try PureXML.Schema.Document(xsd).validate(xml) + } + + @Test("A list simple type validates each whitespace-separated item") + func test_listType() throws { + let xsd = """ + + + + + + + """ + #expect(try validate(xsd, "1 2 3").isEmpty) + #expect(try !validate(xsd, "1 x 3").isEmpty) + } + + @Test("A restricted list bounds its item count") + func test_listLength() throws { + let xsd = """ + + + + + + + + + + + + """ + #expect(try validate(xsd, "

a b

").isEmpty) + #expect(try !validate(xsd, "

a b c

").isEmpty) + } + + @Test("A union admits a value valid for any member type") + func test_unionType() throws { + let xsd = """ + + + + + + + + + + + """ + #expect(try validate(xsd, "10").isEmpty) + #expect(try validate(xsd, "auto").isEmpty) + #expect(try !validate(xsd, "-1").isEmpty) + #expect(try !validate(xsd, "tiny").isEmpty) + } + + @Test("An attribute group is expanded into the referencing type") + func test_attributeGroup() throws { + let xsd = """ + + + + + + + + + + + + """ + #expect(try validate(xsd, "").isEmpty) + #expect(try !validate(xsd, "").isEmpty) + #expect(try !validate(xsd, "").isEmpty) + } + + @Test("A named model group is expanded at its reference") + func test_groupReference() throws { + let xsd = """ + + + + + + + + + + + + + + + + """ + #expect(try validate(xsd, "AB").isEmpty) + #expect(try !validate(xsd, "B").isEmpty) + } + + @Test("An element reference resolves to the global declaration's type") + func test_elementReference() throws { + let xsd = """ + + + + + + + + + + + """ + #expect(try validate(xsd, "12").isEmpty) + #expect(try !validate(xsd, "-1").isEmpty) + #expect(try !validate(xsd, "1").isEmpty) + } +}