PureXML/Sources/Schema/XSDSimpleParser.swift
Mihaela Mihaljevic c9f41c387e fix(schema): honor empty-element default/fixed values and replace enumeration on restriction
An element with a default or fixed value takes that value when it appears empty,
so the empty content is valid even where the type would reject an empty string.
The constraint value, not the empty string, is validated against the type,
including any xsi:type override, so a fixed value invalid against the overriding
type is still rejected.

Validating the constraint value under xsi:type exposed a second defect: a
restriction's enumeration was unioned with the inherited enumeration, so a
derived type wrongly accepted the base's values. Enumeration now replaces on
restriction (the new set is the value space); pattern still accumulates and is
ANDed across derivation steps.

XSTS: valid instances rejected 395 to 369, invalid instances accepted 240 to
238. Baseline updated; regression tests cover empty default/fixed, present
content still validated, and the constraint value checked against an xsi:type.

Refs #147, #129.
2026-06-13 18:04:25 +02:00

114 lines
5.5 KiB
Swift

extension PureXML.Schema {
/// Parses XSD simple types: atomic restrictions with the full facet set, and
/// the `list` and `union` varieties. Kept beside ``XSDParser``, sharing its
/// module-scope tree helpers and parsing context.
enum XSDSimpleParser {
static func simpleType(_ node: XSDTree, _ context: XSDContext) -> 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)
}
// Resolve the base through simpleTypeReference so the built-in list
// datatypes (NMTOKENS, IDREFS, ENTITIES) keep their list variety: a
// length facet on one of them counts list items, not characters (#146).
// Restricting a user type inherits its base, facets, and variety.
let baseType = simpleTypeReference(XSDNode.attribute(restriction, "base") ?? "string", context)
var facets = baseType.facets
// A restriction's own `enumeration` replaces the inherited set (the new
// enumeration is the value space, not a union with the base's), whereas
// `pattern` accumulates across steps and is ANDed. So drop the inherited
// enumeration when this step declares its own.
if XSDNode.elementChildren(restriction).contains(where: { XSDNode.localName($0) == "enumeration" }) {
facets.enumeration = nil
}
applyFacets(restriction, into: &facets)
return SimpleType(base: baseType.base, facets: facets, variety: baseType.variety)
}
static func simpleTypeReference(_ typeName: String, _ context: XSDContext) -> SimpleType {
let local = XSDNode.stripPrefix(typeName)
if let builtin = BuiltinType(rawValue: local) { return SimpleType(base: builtin) }
if let item = listBuiltinItem(local) { return .list(item: SimpleType(base: item)) }
return context.simpleTypes[local] ?? SimpleType(base: .string)
}
/// The item type of a built-in list datatype (`IDREFS`, `ENTITIES`,
/// `NMTOKENS`), each a whitespace-separated list of its singular form.
static func listBuiltinItem(_ name: String) -> BuiltinType? {
switch name {
case "IDREFS": .idref
case "ENTITIES": .entity
case "NMTOKENS": .nmtoken
default: nil
}
}
static func applyFacets(_ restriction: XSDTree, 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 listType(_ node: XSDTree, _ context: XSDContext) -> 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: XSDTree, _ context: XSDContext) -> 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) }
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
}
}
}
}