feat(xsd): add list/union simple types, attribute groups, and element/group refs
Extend the schema parser toward full structural parity: - list simple types validate each whitespace-separated item against the item type and count items for the length facets; union simple types admit a value valid for any member type. A restriction over a named list or union inherits its variety. - Named attribute groups are expanded (transitively) into every complex type that references them. - Named model groups are expanded at each xs:group ref, with a cycle guard; element refs resolve to the referenced global element's declared type, through a reserved type-table key so forward and recursive refs settle at validation time. Named simple types now resolve over repeated passes so they may restrict or list one another regardless of document order. Simple-type and facet parsing moves into a sibling XSDSimpleParser to keep each parser within bounds. Closes #65
This commit is contained in:
parent
df9457d323
commit
cadc63ba8b
@ -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 {
|
||||
|
||||
@ -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<String> = []
|
||||
|
||||
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<String> = []) -> [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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
124
Tests/XSDCompositionTests.swift
Normal file
124
Tests/XSDCompositionTests.swift
Normal file
@ -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 = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:simpleType name="ints">
|
||||
<xs:list itemType="xs:integer"/>
|
||||
</xs:simpleType>
|
||||
<xs:element name="nums" type="ints"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(try validate(xsd, "<nums>1 2 3</nums>").isEmpty)
|
||||
#expect(try !validate(xsd, "<nums>1 x 3</nums>").isEmpty)
|
||||
}
|
||||
|
||||
@Test("A restricted list bounds its item count")
|
||||
func test_listLength() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:simpleType name="pair">
|
||||
<xs:restriction base="couples">
|
||||
<xs:length value="2"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
<xs:simpleType name="couples">
|
||||
<xs:list itemType="xs:string"/>
|
||||
</xs:simpleType>
|
||||
<xs:element name="p" type="pair"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(try validate(xsd, "<p>a b</p>").isEmpty)
|
||||
#expect(try !validate(xsd, "<p>a b c</p>").isEmpty)
|
||||
}
|
||||
|
||||
@Test("A union admits a value valid for any member type")
|
||||
func test_unionType() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:simpleType name="sizeOrAuto">
|
||||
<xs:union memberTypes="xs:nonNegativeInteger">
|
||||
<xs:simpleType>
|
||||
<xs:restriction base="xs:string"><xs:enumeration value="auto"/></xs:restriction>
|
||||
</xs:simpleType>
|
||||
</xs:union>
|
||||
</xs:simpleType>
|
||||
<xs:element name="w" type="sizeOrAuto"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(try validate(xsd, "<w>10</w>").isEmpty)
|
||||
#expect(try validate(xsd, "<w>auto</w>").isEmpty)
|
||||
#expect(try !validate(xsd, "<w>-1</w>").isEmpty)
|
||||
#expect(try !validate(xsd, "<w>tiny</w>").isEmpty)
|
||||
}
|
||||
|
||||
@Test("An attribute group is expanded into the referencing type")
|
||||
func test_attributeGroup() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:attributeGroup name="dims">
|
||||
<xs:attribute name="w" type="xs:integer" use="required"/>
|
||||
<xs:attribute name="h" type="xs:integer" use="required"/>
|
||||
</xs:attributeGroup>
|
||||
<xs:element name="box">
|
||||
<xs:complexType>
|
||||
<xs:attributeGroup ref="dims"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(try validate(xsd, "<box w=\"3\" h=\"4\"/>").isEmpty)
|
||||
#expect(try !validate(xsd, "<box w=\"3\"/>").isEmpty)
|
||||
#expect(try !validate(xsd, "<box w=\"x\" h=\"4\"/>").isEmpty)
|
||||
}
|
||||
|
||||
@Test("A named model group is expanded at its reference")
|
||||
func test_groupReference() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:group name="titleAndBody">
|
||||
<xs:sequence>
|
||||
<xs:element name="title" type="xs:string"/>
|
||||
<xs:element name="body" type="xs:string"/>
|
||||
</xs:sequence>
|
||||
</xs:group>
|
||||
<xs:element name="doc">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:group ref="titleAndBody"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(try validate(xsd, "<doc><title>A</title><body>B</body></doc>").isEmpty)
|
||||
#expect(try !validate(xsd, "<doc><body>B</body></doc>").isEmpty)
|
||||
}
|
||||
|
||||
@Test("An element reference resolves to the global declaration's type")
|
||||
func test_elementReference() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="sku" type="xs:nonNegativeInteger"/>
|
||||
<xs:element name="order">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element ref="sku" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(try validate(xsd, "<order><sku>1</sku><sku>2</sku></order>").isEmpty)
|
||||
#expect(try !validate(xsd, "<order><sku>-1</sku></order>").isEmpty)
|
||||
#expect(try !validate(xsd, "<order><other>1</other></order>").isEmpty)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user