PureXML/Sources/Schema/SchemaSimpleTypeFacetChecks.swift

138 lines
6.0 KiB
Swift

/// The per-facet validation checks, split out so SimpleType's own body stays
/// within the type-body length budget.
extension PureXML.Schema.SimpleType {
private func boundsError(_ value: String, _: PureXML.Schema.Primitive) -> String? {
let (lower, upper) = base.bounds
guard lower != nil || upper != nil, let decimal = PureXML.Schema.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: PureXML.Schema.Primitive) -> String? {
// length/minLength/maxLength measure characters, octets, or list
// items depending on the type; for QName the spec leaves the unit
// unspecified (XSD 1.0 Datatypes 4.3.1), so like Xerces and the XSTS
// NIST oracle we do not constrain QName values by length.
if case .qName = primitive {
return nil
}
if case .notation = primitive {
return nil
}
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 = PureXML.Schema.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
}
func patternError(_ value: String) -> String? {
for group in facets.patternGroups {
let matched = group.contains { pattern in
guard let regex = try? PureXML.Regex.Pattern(pattern) else { return false }
return regex.matches(value)
}
guard matched else {
let label = group.joined(separator: "|")
return "'\(value)' does not match pattern '\(label)'"
}
}
return nil
}
private func enumerationError(_ value: String, _ primitive: PureXML.Schema.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: PureXML.Schema.Primitive) -> String? {
if case .duration = primitive {
return durationRangeError(value)
}
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
}
/// Ordering facets on `xs:duration`, which is a partial order: a value
/// that is incomparable to the bound does not satisfy it (it is not in
/// the ordered range), so an incomparable result is a violation.
private func durationRangeError(_ value: String) -> String? {
guard let actual = PureXML.Schema.DurationValue(value) else { return nil }
func order(_ bound: String?) -> PureXML.Schema.DurationOrder? {
bound.flatMap(PureXML.Schema.DurationValue.init).map(actual.compare(to:))
}
if let order = order(facets.minInclusive), order == .lessThan || order == .incomparable {
return "\(value) is below the inclusive minimum"
}
if let order = order(facets.maxInclusive), order == .greaterThan || order == .incomparable {
return "\(value) is above the inclusive maximum"
}
if let order = order(facets.minExclusive), order != .greaterThan {
return "\(value) is not above the exclusive minimum"
}
if let order = order(facets.maxExclusive), order != .lessThan {
return "\(value) is not below the exclusive maximum"
}
return nil
}
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)
}
}