Make XSD validation namespace-aware and enforce the form defaults and xs:all structural constraints that were previously ignored: - targetNamespace is tracked: a schema's global elements are in its target namespace, and an instance's root element must be in it. - elementFormDefault / attributeFormDefault (and per-declaration form) qualify local element and attribute declarations. The content-model matcher already compares namespace URIs, so qualified declarations now match only same-namespace instance nodes; attribute matching became namespace-aware too. - xs:all groups are checked at compile time: every member must be an element occurring at most once, the group may occur at most once, and nested model groups inside an all are rejected (a SchemaError). When a schema has no target namespace the stamping is a no-op, so unqualified schemas validate exactly as before. Cross-namespace type references through xs:import resolve by local name only; full multi-namespace type tables are not modeled. Closes #78
126 lines
6.9 KiB
Swift
126 lines
6.9 KiB
Swift
public extension PureXML.Schema {
|
|
/// An error compiling or applying a schema.
|
|
enum SchemaError: Swift.Error, Equatable, Sendable, CustomStringConvertible {
|
|
case notASchema
|
|
/// A type derives from a base that declares that derivation method `final`.
|
|
case finalViolation(type: String, base: String, method: String)
|
|
/// A type inside `xs:redefine` does not redefine itself: its restriction or
|
|
/// extension base must name the type it redefines.
|
|
case redefineIncompatible(type: String)
|
|
/// An `xs:all` group violates its XSD 1.0 constraints: every member must be
|
|
/// an element with `maxOccurs` at most 1, and the group itself may occur at
|
|
/// most once.
|
|
case invalidAllGroup(reason: String)
|
|
|
|
public var description: String {
|
|
switch self {
|
|
case .notASchema:
|
|
"the document is not an xs:schema"
|
|
case let .finalViolation(type, base, method):
|
|
"type '\(type)' derives from '\(base)' by \(method), which '\(base)' declares final"
|
|
case let .redefineIncompatible(type):
|
|
"redefined type '\(type)' must derive from itself"
|
|
case let .invalidAllGroup(reason):
|
|
"invalid xs:all group: \(reason)"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A compiled XSD schema: its global element declarations and named-type
|
|
/// table, parsed from a schema document, used to validate instance documents.
|
|
struct Document: Sendable {
|
|
private let elements: [String: ElementType]
|
|
private let types: [String: ElementType]
|
|
private let constraints: [String: [IdentityConstraint]]
|
|
private let nillableElements: Set<String>
|
|
private let elementConstraints: [String: ValueConstraint]
|
|
private let abstractTypes: Set<String>
|
|
private let abstractElements: Set<String>
|
|
private let typeBlock: [String: Set<DerivationMethod>]
|
|
private let typeDerivation: [String: TypeDerivation]
|
|
private let targetNamespace: String?
|
|
|
|
/// Compiles a schema document. `schemaLoader` resolves the
|
|
/// `schemaLocation` of `xs:include`, `xs:import`, and `xs:redefine` to
|
|
/// schema source; it returns nil (the default) when external schemas are
|
|
/// not available, which keeps compilation from reaching the filesystem or
|
|
/// network by default.
|
|
public init(_ xsd: String, schemaLoader: @escaping (String) -> String? = { _ in nil }) throws {
|
|
let compiled = try XSDParser.parse(xsd, loader: schemaLoader)
|
|
elements = compiled.elements
|
|
types = compiled.types
|
|
constraints = compiled.constraints
|
|
nillableElements = compiled.nillableElements
|
|
elementConstraints = compiled.elementConstraints
|
|
abstractTypes = compiled.abstractTypes
|
|
abstractElements = compiled.abstractElements
|
|
typeBlock = compiled.typeBlock
|
|
typeDerivation = compiled.typeDerivation
|
|
targetNamespace = compiled.targetNamespace
|
|
}
|
|
|
|
/// Validates an instance document against the schema, returning one located
|
|
/// ``PureXML/Validation/ValidationError`` per violation. Reports an error
|
|
/// when the root element has no global declaration.
|
|
public func validate(_ xml: String) throws -> [PureXML.Validation.ValidationError] {
|
|
try validate(PureXML.parse(xml))
|
|
}
|
|
|
|
/// Validates an already-parsed node tree, so a caller can validate the same
|
|
/// (possibly recovered) tree it is editing. See ``validate(_:)-(String)``.
|
|
public func validate(_ node: PureXML.Model.Node) -> [PureXML.Validation.ValidationError] {
|
|
guard case let .document(children) = node, let root = children.compactMap(\.element).first else {
|
|
return [.init(reason: "the document has no root element", at: [])]
|
|
}
|
|
guard let declaration = elements[root.name.localName] else {
|
|
return [.init(reason: "no element declaration for '\(root.name.localName)'", at: [])]
|
|
}
|
|
// A schema with a target namespace declares its global elements in that
|
|
// namespace; the root must be in it.
|
|
if let target = targetNamespace, !target.isEmpty, root.name.namespaceURI != target {
|
|
return [.init(reason: "root element '\(root.name.localName)' is not in the schema target namespace '\(target)'", at: [.element(root.name.description)])]
|
|
}
|
|
// An abstract element may not appear in an instance directly; only a
|
|
// concrete substitution-group member may stand in its place.
|
|
if abstractElements.contains(root.name.localName) {
|
|
return [.init(reason: "abstract element '\(root.name.localName)' must not appear in an instance", at: [.element(root.name.description)])]
|
|
}
|
|
let context = PureXML.Validation.XSDContext(
|
|
types: types,
|
|
constraints: constraints,
|
|
rootDeclaration: declaration,
|
|
nillableElements: nillableElements,
|
|
elementConstraints: elementConstraints,
|
|
abstractTypes: abstractTypes,
|
|
typeBlock: typeBlock,
|
|
typeDerivation: typeDerivation,
|
|
)
|
|
return PureXML.Validation.XSD.validator().errors(for: .element(root), in: context)
|
|
}
|
|
|
|
/// What the schema allows at the element a coding `path` addresses in
|
|
/// `tree`: the next child elements (the content-model follow-set), whether
|
|
/// the content may end there, and the declared attributes with their
|
|
/// required/present status. For editor completions and "what's missing".
|
|
/// Returns nil when the path does not address a declared element.
|
|
public func completions(at path: [PureXML.Validation.PathKey], in tree: PureXML.Model.TreeNode) -> Completions? {
|
|
guard let node = tree.node(at: path), case let .element(element) = node.node,
|
|
let type = CompletionEngine.elementType(at: path, elements: elements, types: types)
|
|
else {
|
|
return nil
|
|
}
|
|
return CompletionEngine.completions(for: element, type: type, types: types)
|
|
}
|
|
|
|
/// The quick-fixes the schema offers at the element a coding `path`
|
|
/// addresses in `tree`: add a required-but-absent attribute, or insert a
|
|
/// still-expected required child. Derived from the structured
|
|
/// ``completions(at:in:)``, with precise placement from the node's content
|
|
/// span, so the edits are exact, not guessed from a message.
|
|
public func quickFixes(at path: [PureXML.Validation.PathKey], in tree: PureXML.Model.TreeNode) -> [PureXML.QuickFix] {
|
|
guard let completions = completions(at: path, in: tree), let element = tree.node(at: path) else { return [] }
|
|
return PureXML.QuickFixEngine.fixes(from: completions, element: element)
|
|
}
|
|
}
|
|
}
|