/// 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) } }