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
220 lines
9.8 KiB
Swift
220 lines
9.8 KiB
Swift
public extension PureXML.Schema {
|
|
/// The constraining facets of a simple type. Every facet is optional; an unset
|
|
/// facet imposes no constraint. `patterns` are combined with AND.
|
|
struct Facets: Sendable {
|
|
public var length: Int?
|
|
public var minLength: Int?
|
|
public var maxLength: Int?
|
|
public var patterns: [String]
|
|
public var enumeration: [String]?
|
|
public var whiteSpace: WhiteSpace?
|
|
public var minInclusive: String?
|
|
public var maxInclusive: String?
|
|
public var minExclusive: String?
|
|
public var maxExclusive: String?
|
|
public var totalDigits: Int?
|
|
public var fractionDigits: Int?
|
|
|
|
public init(
|
|
length: Int? = nil,
|
|
minLength: Int? = nil,
|
|
maxLength: Int? = nil,
|
|
patterns: [String] = [],
|
|
enumeration: [String]? = nil,
|
|
whiteSpace: WhiteSpace? = nil,
|
|
minInclusive: String? = nil,
|
|
maxInclusive: String? = nil,
|
|
minExclusive: String? = nil,
|
|
maxExclusive: String? = nil,
|
|
totalDigits: Int? = nil,
|
|
fractionDigits: Int? = nil,
|
|
) {
|
|
self.length = length
|
|
self.minLength = minLength
|
|
self.maxLength = maxLength
|
|
self.patterns = patterns
|
|
self.enumeration = enumeration
|
|
self.whiteSpace = whiteSpace
|
|
self.minInclusive = minInclusive
|
|
self.maxInclusive = maxInclusive
|
|
self.minExclusive = minExclusive
|
|
self.maxExclusive = maxExclusive
|
|
self.totalDigits = totalDigits
|
|
self.fractionDigits = fractionDigits
|
|
}
|
|
}
|
|
|
|
/// 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(), 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.
|
|
public func isValid(_ lexical: String) -> Bool {
|
|
validate(lexical) == nil
|
|
}
|
|
|
|
/// 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 {
|
|
return "'\(lexical)' is not a valid \(base.rawValue)"
|
|
}
|
|
return boundsError(value, primitive)
|
|
?? lengthError(value, primitive)
|
|
?? digitsError(value)
|
|
?? patternError(value)
|
|
?? enumerationError(value, primitive)
|
|
?? rangeError(value, primitive)
|
|
}
|
|
|
|
// MARK: Whitespace
|
|
|
|
static func process(_ value: String, whiteSpace: WhiteSpace) -> String {
|
|
switch whiteSpace {
|
|
case .preserve:
|
|
return value
|
|
case .replace:
|
|
return String(value.map { ($0 == "\t" || $0 == "\n" || $0 == "\r") ? " " : $0 })
|
|
case .collapse:
|
|
let replaced = value.map { ($0 == "\t" || $0 == "\n" || $0 == "\r") ? " " : $0 }
|
|
return String(replaced).split(separator: " ").joined(separator: " ")
|
|
}
|
|
}
|
|
|
|
// MARK: Facet checks
|
|
|
|
private func boundsError(_ value: String, _: Primitive) -> String? {
|
|
let (lower, upper) = base.bounds
|
|
guard lower != nil || upper != nil, let decimal = DecimalValue(value, allowFraction: false) else {
|
|
return nil
|
|
}
|
|
if let lower, decimal < lower { return "\(value) is below the minimum for \(base.rawValue)" }
|
|
if let upper, decimal > upper { return "\(value) is above the maximum for \(base.rawValue)" }
|
|
return nil
|
|
}
|
|
|
|
private func lengthError(_ value: String, _ primitive: Primitive) -> String? {
|
|
let length = primitive.measuredLength(value)
|
|
if let exact = facets.length, length != exact { return "length \(length) is not \(exact)" }
|
|
if let minimum = facets.minLength, length < minimum { return "length \(length) is below \(minimum)" }
|
|
if let maximum = facets.maxLength, length > maximum { return "length \(length) is above \(maximum)" }
|
|
return nil
|
|
}
|
|
|
|
private func digitsError(_ value: String) -> String? {
|
|
guard facets.totalDigits != nil || facets.fractionDigits != nil,
|
|
let decimal = DecimalValue(value, allowFraction: true) else { return nil }
|
|
if let total = facets.totalDigits, decimal.totalDigits > total { return "more than \(total) total digits" }
|
|
if let fraction = facets.fractionDigits, decimal.fractionDigits.count > fraction {
|
|
return "more than \(fraction) fraction digits"
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func patternError(_ value: String) -> String? {
|
|
for pattern in facets.patterns {
|
|
guard let regex = try? PureXML.Regex.Pattern(pattern), regex.matches(value) else {
|
|
return "'\(value)' does not match pattern '\(pattern)'"
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func enumerationError(_ value: String, _ primitive: Primitive) -> String? {
|
|
guard let enumeration = facets.enumeration else { return nil }
|
|
let whiteSpace = facets.whiteSpace ?? base.whiteSpace
|
|
let matched = enumeration.contains { candidate in
|
|
let normalized = Self.process(candidate, whiteSpace: whiteSpace)
|
|
if let lhs = primitive.ordered(value), let rhs = primitive.ordered(normalized) { return lhs == rhs }
|
|
return normalized == value
|
|
}
|
|
return matched ? nil : "'\(value)' is not in the enumeration"
|
|
}
|
|
|
|
private func rangeError(_ value: String, _ primitive: Primitive) -> String? {
|
|
guard let actual = primitive.ordered(value) else { return nil }
|
|
if let bound = facets.minInclusive, let limit = primitive.ordered(bound), actual < limit {
|
|
return "\(value) is below the inclusive minimum"
|
|
}
|
|
if let bound = facets.maxInclusive, let limit = primitive.ordered(bound), actual > limit {
|
|
return "\(value) is above the inclusive maximum"
|
|
}
|
|
if let bound = facets.minExclusive, let limit = primitive.ordered(bound), !(limit < actual) {
|
|
return "\(value) is not above the exclusive minimum"
|
|
}
|
|
if let bound = facets.maxExclusive, let limit = primitive.ordered(bound), !(actual < limit) {
|
|
return "\(value) is not below the exclusive maximum"
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Validates `value` against a built-in type with no extra facets.
|
|
static func isValid(_ value: String, type: BuiltinType) -> Bool {
|
|
SimpleType(base: type).isValid(value)
|
|
}
|
|
}
|