PureXML/Sources/Schema/SchemaDocument.swift
Mihaela Mihaljevic fe19b0b1ea feat(xsd): add identity constraints (unique, key, keyref)
Validate XSD identity constraints over the instance document in a pass that
walks the tree alongside type validation:

- xs:unique requires the selected nodes' field tuples to be distinct, excluding
  any node that is missing a field.
- xs:key additionally requires every field to be present.
- xs:keyref requires each tuple to equal a tuple of the named key or unique
  visible in an enclosing scope, resolved through a stack of scope frames so a
  keyref and the key it refers to may sit on the same element.

The selector and field XPaths run on the existing XPath engine, relative to the
declaring element and each selected node. Constraints are gathered for every
element declaration at any depth, keyed by element name.

Closes #67
2026-06-08 08:58:36 +02:00

50 lines
2.3 KiB
Swift

public extension PureXML.Schema {
/// An error compiling or applying a schema.
enum SchemaError: Swift.Error, Equatable, Sendable, CustomStringConvertible {
case notASchema
public var description: String {
switch self {
case .notASchema: "the document is not an xs:schema"
}
}
}
/// 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]]
/// 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
}
/// Validates an instance document against the schema, returning one issue
/// per violation. Reports an issue when the root element has no global
/// declaration.
public func validate(_ xml: String) throws -> [PureXML.Validation.Issue] {
guard case let .document(children) = try PureXML.parse(xml),
let root = children.compactMap(\.element).first
else {
return [.init(severity: .error, message: "the document has no root element")]
}
guard let declaration = elements[root.name.localName] else {
return [.init(severity: .error, message: "no element declaration for '\(root.name.localName)'")]
}
var issues = ComplexValidator(types: types).validate(root, as: declaration)
issues += IdentityValidator(constraints: constraints).validate(PureXML.Model.TreeNode(.element(root)))
return issues
}
}
}