Consolidates the in-progress #146 work; valid-instances-rejected reaches 4 on the 2006-11-06 XSTS archive (buckets now 1/262/4/147). Regex: match Unicode scalars rather than grapheme clusters; the empty pattern matches only the empty string; pin the category repertoire to Unicode 3.2, the version the corpus is consistent at, so code points added in 3.2 (Tagalog, Cyrillic Komi) are word characters while later additions (the Tamil digit zero) stay unassigned and match \D. New XSDCategory and a 3.2 assignment table derived from DerivedAge, replacing the old Unicode31 files. Content model: assess each child against the particle the content automaton matched, not a by-name lookup (#180). Two same-named particles keep their own fixed values, and an element matching a skip wildcard is no longer validated against its global declaration. Selection is made reproducible when an ambiguous schema slips past the UPA check. XSTS runner: compile and merge every schemaDocument in a schemaTest, not just the first; this also surfaced one previously-masked invalid-instance acceptance (recorded in the baselines). Also folds in attribute-group anyAttribute inheritance, extension wildcard union, namespace-qualified type tables for multi-document xsi:schemaLocation, per-container elementFormDefault, strict and lax attribute wildcard validation, built-in type resolution, and xs:redefine simpleType overrides.
191 lines
9.1 KiB
Swift
191 lines
9.1 KiB
Swift
public extension PureXML.Schema {
|
|
/// An element paired with its already-resolved effective type: the subject of
|
|
/// the streaming XSD content rule, so the contextual type resolution that a
|
|
/// node-tree walk cannot express is carried on the subject instead.
|
|
struct ResolvedElement: PureXML.Validation.Validatable {
|
|
public let element: PureXML.Model.Element
|
|
public let type: ElementType
|
|
|
|
public init(element: PureXML.Model.Element, type: ElementType) {
|
|
self.element = element
|
|
self.type = type
|
|
}
|
|
}
|
|
}
|
|
|
|
public extension PureXML.Schema.ComplexValidator {
|
|
// MARK: Streaming support
|
|
|
|
/// The streaming XSD content check as a composable ``PureXML/Validation`` value
|
|
/// (the OpenAPIKit idiom): an element is valid against its resolved type. The
|
|
/// validator is the document, the cross-cutting context that holds the type
|
|
/// tables, so the streaming path is rule-driven like every other validator
|
|
/// rather than a bare method call.
|
|
static var shallowValidity: PureXML.Validation.Validation<PureXML.Schema.ResolvedElement, PureXML.Schema.ComplexValidator> {
|
|
.init(description: "Each streamed element is valid against its declared XSD type") { context in
|
|
context.document.validateShallow(context.subject.element, as: context.subject.type, at: context.codingPath)
|
|
}
|
|
}
|
|
|
|
/// The effective element type after an `xsi:type` override and following any
|
|
/// `typeReference` chain, so the streaming driver resolves a type once per
|
|
/// element rather than re-entering the tree walk.
|
|
func effectiveType(_ declared: PureXML.Schema.ElementType, of element: PureXML.Model.Element) -> PureXML.Schema.ElementType {
|
|
var type = declared
|
|
if let overriding = Self.xsiTypeName(element), let resolved = types[overriding] {
|
|
type = resolved
|
|
}
|
|
// An unknown or circular chain stays a typeReference, which
|
|
// validateShallow reports as a located error.
|
|
if case let .resolved(resolved) = resolveReference(type) {
|
|
type = resolved
|
|
}
|
|
return type
|
|
}
|
|
|
|
/// The declared element type of a child `name` in `parent`'s content model: a
|
|
/// named particle member, or a global declaration reached through a matching
|
|
/// wildcard's `processContents`. Returns nil when the parent has no element
|
|
/// content, when `processContents="skip"`, or when the child is not declared
|
|
/// and no wildcard admits it (the parent's structure check reports that).
|
|
func childType(of parent: PureXML.Schema.ElementType, child name: PureXML.Model.QualifiedName) -> PureXML.Schema.ElementType? {
|
|
guard case let .complex(complex) = parent else { return nil }
|
|
switch complex.content {
|
|
case let .elementOnly(particle), let .mixed(particle):
|
|
if let type = Self.elementTypes(in: particle.term)[Self.key(name)] {
|
|
return type
|
|
}
|
|
return wildcardChildType(for: name, in: particle.term)
|
|
case .empty, .simpleContent:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// The error when a strict wildcard admits a child but no global declaration
|
|
/// exists for it, matching the tree validator's ``validateWildcardChild``.
|
|
func strictWildcardError(
|
|
for child: PureXML.Model.QualifiedName,
|
|
in parent: PureXML.Schema.ElementType,
|
|
at path: [PureXML.Validation.PathKey],
|
|
) -> PureXML.Validation.ValidationError? {
|
|
guard case let .complex(complex) = parent else { return nil }
|
|
switch complex.content {
|
|
case let .elementOnly(particle), let .mixed(particle):
|
|
if Self.elementTypes(in: particle.term)[Self.key(child)] != nil { return nil }
|
|
guard Self.wildcardMatch(for: child, in: particle.term) == .strict,
|
|
types["element:\(child.localName)"] == nil
|
|
else { return nil }
|
|
return PureXML.Validation.ValidationError(
|
|
reason: "no declaration for wildcard-matched element '\(child.localName)'",
|
|
at: path,
|
|
)
|
|
case .empty, .simpleContent:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// The `processContents` of a wildcard in `term` that admits `name`, if any.
|
|
static func wildcardMatch(for name: PureXML.Model.QualifiedName, in term: PureXML.Schema.Term) -> PureXML.Schema.ProcessContents? {
|
|
switch term {
|
|
case let .wildcard(wildcard):
|
|
return wildcard.admits(name) ? wildcard.processContents : nil
|
|
case .element:
|
|
return nil
|
|
case let .group(group):
|
|
for member in group.particles {
|
|
if let match = wildcardMatch(for: name, in: member.term) { return match }
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private func wildcardChildType(for name: PureXML.Model.QualifiedName, in term: PureXML.Schema.Term) -> PureXML.Schema.ElementType? {
|
|
guard let process = Self.wildcardMatch(for: name, in: term) else { return nil }
|
|
switch process {
|
|
case .skip:
|
|
return nil
|
|
case .lax, .strict:
|
|
return types[PureXML.Schema.XSDParser.elementDeclarationKey(name)]
|
|
?? types["element:\(name.localName)"]
|
|
}
|
|
}
|
|
|
|
/// Validates one element's own attributes and content-model structure (child
|
|
/// names, order, occurrence) and simple content, but not its children's own
|
|
/// validity, which the streaming driver checks as each child closes. `type`
|
|
/// must already be effective (see ``effectiveType(_:of:)``).
|
|
func validateShallow(
|
|
_ element: PureXML.Model.Element,
|
|
as type: PureXML.Schema.ElementType,
|
|
at path: [PureXML.Validation.PathKey] = [],
|
|
) -> [PureXML.Validation.ValidationError] {
|
|
var errors: [XSDFailure] = []
|
|
switch type {
|
|
case let .simple(simple):
|
|
validateSimpleElement(element, simple, at: path, into: &errors)
|
|
case let .complex(complex):
|
|
validateAttributes(element, complex, at: path, into: &errors)
|
|
if let nilErrors = nilErrors(element, at: path) { return errors + nilErrors }
|
|
shallowContent(element, complex.content, at: path, into: &errors)
|
|
errors += elementFixedErrors(element, at: path)
|
|
case .typeReference:
|
|
// The shared resolver also guards a circular chain, which would
|
|
// otherwise recurse here without terminating.
|
|
switch resolveReference(type) {
|
|
case let .unknown(name):
|
|
return [XSDFailure(reason: "unknown type '\(name)'", at: path)]
|
|
case let .circular(name):
|
|
return [XSDFailure(reason: "circular type reference '\(name)'", at: path)]
|
|
case let .resolved(resolved):
|
|
return validateShallow(element, as: resolved, at: path)
|
|
}
|
|
}
|
|
return errors
|
|
}
|
|
|
|
private func validateSimpleElement(_ element: PureXML.Model.Element, _ simple: PureXML.Schema.SimpleType, at path: XSDPath, into errors: inout [XSDFailure]) {
|
|
if let nilErrors = nilErrors(element, at: path) {
|
|
errors += nilErrors
|
|
return
|
|
}
|
|
if !element.children.compactMap(\.element).isEmpty {
|
|
errors.append(XSDFailure(reason: "'\(element.name.localName)' must not have children", at: path))
|
|
}
|
|
let text = Self.rawTextContent(element)
|
|
if let error = simple.validate(text) {
|
|
errors.append(XSDFailure(reason: "'\(element.name.localName)': \(error)", at: path))
|
|
}
|
|
recordIDs(simple, value: text, at: path)
|
|
errors += elementFixedErrors(element, at: path)
|
|
}
|
|
|
|
private func shallowContent(_ element: PureXML.Model.Element, _ content: PureXML.Schema.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)) }
|
|
let text = Self.rawTextContent(element)
|
|
if let error = type.validate(text) {
|
|
errors.append(XSDFailure(reason: "content: \(error)", at: path))
|
|
}
|
|
recordIDs(type, value: text, at: path)
|
|
case let .elementOnly(particle):
|
|
rejectText(element, at: path, into: &errors)
|
|
shallowStructure(particle, children: children, at: path, into: &errors)
|
|
case let .mixed(particle):
|
|
shallowStructure(particle, children: children, at: path, into: &errors)
|
|
}
|
|
}
|
|
|
|
private func shallowStructure(_ particle: PureXML.Schema.Particle, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFailure]) {
|
|
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)
|
|
}
|
|
}
|
|
}
|