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.
385 lines
19 KiB
Swift
385 lines
19 KiB
Swift
/// File-scope aliases for the XSD complex-type validator, kept out of the
|
|
/// namespace to avoid nesting a type two levels deep.
|
|
typealias XSDFailure = PureXML.Validation.ValidationError
|
|
typealias XSDPath = [PureXML.Validation.PathKey]
|
|
private typealias XSDGroup = PureXML.Schema.Group
|
|
private typealias XSDTerm = PureXML.Schema.Term
|
|
private typealias XSDTermLabel = PureXML.Schema.TermLabel
|
|
private typealias XSDProcessContents = PureXML.Schema.ProcessContents
|
|
private typealias XSDElementType = PureXML.Schema.ElementType
|
|
private typealias XSDWildcard = PureXML.Schema.Wildcard
|
|
|
|
public extension PureXML.Schema {
|
|
/// Validates an element against a ``ComplexType``: its attribute uses, its
|
|
/// content model, and, recursively, each child element against the type
|
|
/// declared for its name. Every violation is located by the element's coding
|
|
/// path, and the results are ``PureXML/Validation/ValidationError`` values.
|
|
struct ComplexValidator {
|
|
/// The named type table that `ElementType.typeReference` resolves against.
|
|
let types: [String: ElementType]
|
|
/// Local names of elements declared `nillable="true"`.
|
|
private let nillableElements: Set<String>
|
|
/// The `default`/`fixed` value constraint declared on each element name.
|
|
private let elementConstraints: [String: ValueConstraint]
|
|
/// Local names of complex types declared `abstract="true"`.
|
|
let abstractTypes: Set<String>
|
|
/// Derivation methods each named type forbids through `xsi:type`.
|
|
let typeBlock: [String: Set<DerivationMethod>]
|
|
/// Derivation methods each element declaration forbids through `xsi:type`
|
|
/// (the `block` on the element, distinct from the block on its type).
|
|
let elementBlock: [String: Set<DerivationMethod>]
|
|
/// Each named complex type's base and derivation method.
|
|
let typeDerivation: [String: TypeDerivation]
|
|
|
|
public init(
|
|
types: [String: ElementType] = [:],
|
|
nillableElements: Set<String> = [],
|
|
elementConstraints: [String: ValueConstraint] = [:],
|
|
abstractTypes: Set<String> = [],
|
|
typeBlock: [String: Set<DerivationMethod>] = [:],
|
|
elementBlock: [String: Set<DerivationMethod>] = [:],
|
|
typeDerivation: [String: TypeDerivation] = [:],
|
|
) {
|
|
self.types = types
|
|
self.nillableElements = nillableElements
|
|
self.elementConstraints = elementConstraints
|
|
self.abstractTypes = abstractTypes
|
|
self.typeBlock = typeBlock
|
|
self.elementBlock = elementBlock
|
|
self.typeDerivation = typeDerivation
|
|
}
|
|
|
|
/// Validates `element` against `type` at `path`, one error per violation.
|
|
public func validate(
|
|
_ element: PureXML.Model.Element,
|
|
against type: ComplexType,
|
|
at path: [PureXML.Validation.PathKey] = [],
|
|
) -> [PureXML.Validation.ValidationError] {
|
|
var errors: [XSDFailure] = []
|
|
validateAttributes(element, type, at: path, into: &errors)
|
|
// An xsi:nil element keeps its attribute obligations but must have no
|
|
// content; its content model is not otherwise checked.
|
|
if let nilErrors = nilErrors(element, at: path) {
|
|
return errors + nilErrors
|
|
}
|
|
validateContent(element, type.content, at: path, into: &errors)
|
|
errors += elementFixedErrors(element, at: path)
|
|
return errors
|
|
}
|
|
|
|
/// The errors from an `xsi:nil="true"` element: rejecting it when the
|
|
/// element is not nillable, or when it carries content. Returns nil when
|
|
/// the element is not nilled.
|
|
func nilErrors(_ element: PureXML.Model.Element, at path: XSDPath) -> [XSDFailure]? {
|
|
guard Self.isNil(element) else { return nil }
|
|
let name = element.name.localName
|
|
if !nillableElements.contains(name) {
|
|
return [XSDFailure(reason: "element '\(name)' is not nillable but has xsi:nil", at: path)]
|
|
}
|
|
if Self.hasContent(element) {
|
|
return [XSDFailure(reason: "nil element '\(name)' must be empty", at: path)]
|
|
}
|
|
return []
|
|
}
|
|
|
|
/// The error from a `fixed` element value constraint: the element's text
|
|
/// must equal the fixed value.
|
|
func elementFixedErrors(_ element: PureXML.Model.Element, at path: XSDPath) -> [XSDFailure] {
|
|
guard let fixed = elementConstraints[element.name.localName]?.fixedValue else { return [] }
|
|
let text = Self.textContent(element)
|
|
// An empty element takes the fixed value; only present content must equal it.
|
|
if text.isEmpty { return [] }
|
|
return text == fixed ? [] : [XSDFailure(reason: "element '\(element.name.localName)' is fixed and must be '\(fixed)'", at: path)]
|
|
}
|
|
|
|
/// Validates `element` against any element type at `path`, resolving a
|
|
/// `typeReference` through the type table.
|
|
public func validate(
|
|
_ element: PureXML.Model.Element,
|
|
as type: ElementType,
|
|
at path: [PureXML.Validation.PathKey] = [],
|
|
) -> [PureXML.Validation.ValidationError] {
|
|
var errors: [XSDFailure] = []
|
|
validateChild(element, against: type, at: path, into: &errors)
|
|
return errors
|
|
}
|
|
|
|
// MARK: Attributes
|
|
|
|
func validateAttributes(
|
|
_ element: PureXML.Model.Element,
|
|
_ type: ComplexType,
|
|
at path: XSDPath,
|
|
into errors: inout [XSDFailure],
|
|
) {
|
|
let present = element.attributes.filter { !Self.isNamespaceDeclaration($0) && !Self.isSchemaInstanceAttribute($0) }
|
|
for use in type.attributes {
|
|
let match = present.first { Self.sameName($0.name, use.name) }
|
|
if let match {
|
|
if let error = use.type.validate(match.value) {
|
|
errors.append(XSDFailure(reason: "attribute '\(use.name.localName)': \(error)", at: path))
|
|
}
|
|
if let fixed = use.valueConstraint?.fixedValue, match.value != fixed {
|
|
errors.append(XSDFailure(reason: "attribute '\(use.name.localName)' is fixed and must be '\(fixed)'", at: path))
|
|
}
|
|
} else if use.required {
|
|
errors.append(XSDFailure(reason: "missing required attribute '\(use.name.localName)'", at: path))
|
|
}
|
|
}
|
|
// An undeclared attribute is allowed only if an xs:anyAttribute wildcard
|
|
// admits its namespace.
|
|
for attribute in present where !type.attributes.contains(where: { Self.sameName($0.name, attribute.name) }) {
|
|
if type.attributeWildcard?.admits(attribute.name) == true { continue }
|
|
errors.append(XSDFailure(reason: "undeclared attribute '\(attribute.name.localName)'", at: path))
|
|
}
|
|
}
|
|
|
|
// MARK: Content
|
|
|
|
private func validateContent(
|
|
_ element: PureXML.Model.Element,
|
|
_ content: ContentType,
|
|
at path: XSDPath,
|
|
into errors: inout [XSDFailure],
|
|
) {
|
|
let children = element.children.compactMap(\.element)
|
|
switch content {
|
|
case .empty:
|
|
if !children.isEmpty { errors.append(XSDFailure(reason: "element must be empty", at: path)) }
|
|
rejectText(element, at: path, into: &errors)
|
|
case let .simpleContent(type):
|
|
if !children.isEmpty { errors.append(XSDFailure(reason: "element must not have children", at: path)) }
|
|
if let error = type.validate(Self.textContent(element)) {
|
|
errors.append(XSDFailure(reason: "content: \(error)", at: path))
|
|
}
|
|
case let .elementOnly(particle):
|
|
rejectText(element, at: path, into: &errors)
|
|
validateParticle(particle, children: children, at: path, into: &errors)
|
|
case let .mixed(particle):
|
|
validateParticle(particle, children: children, at: path, into: &errors)
|
|
}
|
|
}
|
|
|
|
func rejectText(_ element: PureXML.Model.Element, at path: XSDPath, into errors: inout [XSDFailure]) {
|
|
guard !Self.textContent(element).isEmpty else { return }
|
|
errors.append(XSDFailure(reason: "element must not contain text", at: path))
|
|
}
|
|
|
|
// MARK: Particles
|
|
|
|
private func validateParticle(
|
|
_ particle: Particle,
|
|
children: [PureXML.Model.Element],
|
|
at path: XSDPath,
|
|
into errors: inout [XSDFailure],
|
|
) {
|
|
// Locate each content-model violation at the offending child (or the
|
|
// missing one), with a recovery hint, rather than one opaque failure,
|
|
// then still validate every well-placed child's own content.
|
|
if case let .group(group) = particle.term, group.compositor == .all {
|
|
allStructureErrors(group, children: children, at: path, into: &errors)
|
|
} else {
|
|
sequenceStructureErrors(particle, children: children, at: path, into: &errors)
|
|
}
|
|
validateChildren(
|
|
children,
|
|
childTypes: Self.elementTypes(in: particle.term),
|
|
wildcard: Self.wildcard(in: particle.term),
|
|
at: path,
|
|
into: &errors,
|
|
)
|
|
}
|
|
|
|
private func validateChildren(
|
|
_ children: [PureXML.Model.Element],
|
|
childTypes: [String: ElementType],
|
|
wildcard: ProcessContents?,
|
|
at path: XSDPath,
|
|
into errors: inout [XSDFailure],
|
|
) {
|
|
let steps = Self.childSteps(children)
|
|
for (child, step) in zip(children, steps) {
|
|
if let declared = childTypes[Self.key(child.name)] {
|
|
validateChild(child, against: declared, at: path + [step], into: &errors)
|
|
} else if let wildcard {
|
|
// The structure already matched, so an undeclared child here was
|
|
// admitted by a wildcard; process its content per the wildcard.
|
|
validateWildcardChild(child, processContents: wildcard, at: path + [step], into: &errors)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Validates a wildcard-matched child by its `processContents`: skip does
|
|
/// nothing, lax validates against a global declaration when one exists, and
|
|
/// strict requires one.
|
|
private func validateWildcardChild(
|
|
_ child: PureXML.Model.Element,
|
|
processContents: ProcessContents,
|
|
at path: XSDPath,
|
|
into errors: inout [XSDFailure],
|
|
) {
|
|
switch processContents {
|
|
case .skip:
|
|
return
|
|
case .lax:
|
|
if let declaration = types["element:\(child.name.localName)"] {
|
|
validateChild(child, against: declaration, at: path, into: &errors)
|
|
}
|
|
case .strict:
|
|
guard let declaration = types["element:\(child.name.localName)"] else {
|
|
errors.append(XSDFailure(reason: "no declaration for wildcard-matched element '\(child.name.localName)'", at: path))
|
|
return
|
|
}
|
|
validateChild(child, against: declaration, at: path, into: &errors)
|
|
}
|
|
}
|
|
|
|
private func validateChild(
|
|
_ child: PureXML.Model.Element,
|
|
against declared: ElementType,
|
|
at path: XSDPath,
|
|
into errors: inout [XSDFailure],
|
|
) {
|
|
// An instance `xsi:type` overrides the declared type, provided the named
|
|
// type exists in the schema. Resolve to the type itself rather than a
|
|
// reference so re-entry does not re-read the same xsi:type. A `block` on
|
|
// the declared type can forbid the substitution; an abstract declared
|
|
// type requires one.
|
|
if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path) {
|
|
errors.append(blocked)
|
|
return
|
|
}
|
|
if case let .typeReference(name) = declared, let missing = abstractTypeError(named: name, child: child, at: path) {
|
|
errors.append(missing)
|
|
return
|
|
}
|
|
guard let declared = overriddenType(declared, for: child, at: path, into: &errors) else { return }
|
|
switch declared {
|
|
case let .simple(simple):
|
|
if let nilErrors = nilErrors(child, at: path) {
|
|
errors += nilErrors
|
|
return
|
|
}
|
|
if !child.children.compactMap(\.element).isEmpty {
|
|
errors.append(XSDFailure(reason: "'\(child.name.localName)' must not have children", at: path))
|
|
}
|
|
// An empty element takes its `default`/`fixed` value; that value
|
|
// (not the empty string) is what must be valid against the type,
|
|
// including any xsi:type override already resolved into `simple`.
|
|
let text = Self.textContent(child)
|
|
let effective = text.isEmpty ? (elementConstraints[child.name.localName]?.value ?? text) : text
|
|
if let error = simple.validate(effective) {
|
|
errors.append(XSDFailure(reason: "'\(child.name.localName)': \(error)", at: path))
|
|
}
|
|
errors += elementFixedErrors(child, at: path)
|
|
case let .complex(complex):
|
|
errors.append(contentsOf: validate(child, against: complex, at: path))
|
|
case .typeReference:
|
|
validateResolvedReference(declared, child, at: path, into: &errors)
|
|
}
|
|
}
|
|
|
|
/// The declared type after an `xsi:type` override. An override naming an
|
|
/// undeclared type appends an error and returns nil, not a silent fallback
|
|
/// to the declared type.
|
|
private func overriddenType(
|
|
_ declared: ElementType,
|
|
for child: PureXML.Model.Element,
|
|
at path: XSDPath,
|
|
into errors: inout [XSDFailure],
|
|
) -> ElementType? {
|
|
guard let overriding = Self.xsiTypeName(child) else { return declared }
|
|
guard let resolved = types[overriding] else {
|
|
errors.append(XSDFailure(reason: "unknown xsi:type '\(overriding)' on '\(child.name.localName)'", at: path))
|
|
return nil
|
|
}
|
|
return resolved
|
|
}
|
|
|
|
/// Validates a child against a `typeReference` through the shared resolver,
|
|
/// reporting an unknown name or a circular chain as a located error.
|
|
private func validateResolvedReference(
|
|
_ declared: ElementType,
|
|
_ child: PureXML.Model.Element,
|
|
at path: XSDPath,
|
|
into errors: inout [XSDFailure],
|
|
) {
|
|
switch resolveReference(declared) {
|
|
case let .unknown(name):
|
|
errors.append(XSDFailure(reason: "unknown type '\(name)'", at: path))
|
|
case let .circular(name):
|
|
errors.append(XSDFailure(reason: "circular type reference '\(name)'", at: path))
|
|
case let .resolved(resolved):
|
|
validateChild(child, against: resolved, at: path, into: &errors)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Located content-model diagnostics: pinpoint which child breaks the model and
|
|
/// what was expected there, so an editor shows placed errors with recovery hints
|
|
/// rather than one opaque "content does not match" per element.
|
|
extension PureXML.Schema.ComplexValidator {
|
|
/// Walks the children through the content automaton, flagging the first child
|
|
/// the follow-set rejects, or the missing content when the sequence ends early.
|
|
func sequenceStructureErrors(_ particle: PureXML.Schema.Particle, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFailure]) {
|
|
let nfa = PureXML.Schema.ContentNFABuilder.build(particle)
|
|
let steps = Self.childSteps(children)
|
|
// Advance one active state-set across the children rather than re-walking
|
|
// the prefix per child (which is quadratic over the content model, #129).
|
|
var current = nfa.startStates()
|
|
for (index, child) in children.enumerated() {
|
|
guard let next = nfa.step(current, over: child.name) else {
|
|
let allowed = nfa.admissible(from: current)
|
|
errors.append(XSDFailure(reason: "element '\(child.name.localName)' is not allowed here\(Self.expectation(allowed))", at: path + [steps[index]]))
|
|
return
|
|
}
|
|
current = next
|
|
}
|
|
if !nfa.isComplete(current) {
|
|
errors.append(XSDFailure(reason: "content is incomplete\(Self.expectation(nfa.admissible(from: current)))", at: path))
|
|
}
|
|
}
|
|
|
|
/// Locates `all`-group violations: each child that is not an in-bounds member,
|
|
/// recovering past it, then each required member that never appeared.
|
|
func allStructureErrors(_ group: PureXML.Schema.Group, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFailure]) {
|
|
var counts = [Int](repeating: 0, count: group.particles.count)
|
|
let steps = Self.childSteps(children)
|
|
for (index, child) in children.enumerated() {
|
|
guard let position = group.particles.indices.first(where: { slot in
|
|
let member = group.particles[slot]
|
|
let room = member.maxOccurs.map { counts[slot] < $0 } ?? true
|
|
return room && Self.memberMatches(member.term, child.name)
|
|
}) else {
|
|
errors.append(XSDFailure(reason: "element '\(child.name.localName)' is not allowed here", at: path + [steps[index]]))
|
|
continue
|
|
}
|
|
counts[position] += 1
|
|
}
|
|
for (index, member) in group.particles.enumerated() where counts[index] < member.minOccurs {
|
|
if case let .element(name, _) = member.term {
|
|
errors.append(XSDFailure(reason: "element '\(name.localName)' is required but missing", at: path))
|
|
}
|
|
}
|
|
}
|
|
|
|
static func memberMatches(_ term: PureXML.Schema.Term, _ name: PureXML.Model.QualifiedName) -> Bool {
|
|
switch term {
|
|
case let .element(declared, _): declared.localName == name.localName && declared.namespaceURI == name.namespaceURI
|
|
case let .wildcard(wildcard): wildcard.admits(name)
|
|
case .group: false
|
|
}
|
|
}
|
|
|
|
/// A "; expected a, b" hint naming the elements the automaton accepts next.
|
|
static func expectation(_ labels: [PureXML.Schema.TermLabel]) -> String {
|
|
let names = labels.compactMap { label -> String? in
|
|
if case let .name(qualified) = label { return "<\(qualified.localName)>" }
|
|
return nil
|
|
}
|
|
let unique = Set(names).sorted()
|
|
return unique.isEmpty ? "" : "; expected \(unique.joined(separator: ", "))"
|
|
}
|
|
}
|