An xsi:type naming an undeclared type is now a located error in both the tree validator (validateChild) and the streaming driver, instead of a silent fallback to the declared type. typeReference chains resolve through one shared resolver (resolveReference, with a visited set), replacing the inline recursion, the 64-hop cap, and the completions guard: a circular chain now reports 'circular type reference' instead of silently truncating, and the streaming shallow check no longer recurses without bound on a cyclic table (a latent crash). Tests cover unknown xsi:type (tree + streaming agree), the circular chain (tree + shallow), the resolver's three outcomes, and a conformance-harness case pair. Closes #110
126 lines
5.8 KiB
Swift
126 lines
5.8 KiB
Swift
public extension PureXML.Schema {
|
|
/// What a schema allows at an element: the next child elements, whether the
|
|
/// content may end here, and the declared attributes. For editor completions
|
|
/// and "what's missing".
|
|
struct Completions: Sendable, Equatable {
|
|
/// Element names allowed as the next child at the current position, in
|
|
/// content-model order. A content model with a wildcard (`xs:any`) leaves
|
|
/// this empty and sets `allowsAnyElement`.
|
|
public var elements: [String]
|
|
/// Whether the content may legally end here. When false, a required child
|
|
/// is still expected.
|
|
public var complete: Bool
|
|
/// Whether a wildcard permits arbitrary further elements here.
|
|
public var allowsAnyElement: Bool
|
|
/// The declared attributes, each with whether it is required and already
|
|
/// present on the element.
|
|
public var attributes: [AttributeCompletion]
|
|
|
|
public init(elements: [String], complete: Bool, allowsAnyElement: Bool, attributes: [AttributeCompletion]) {
|
|
self.elements = elements
|
|
self.complete = complete
|
|
self.allowsAnyElement = allowsAnyElement
|
|
self.attributes = attributes
|
|
}
|
|
}
|
|
|
|
/// One declared attribute in a ``Completions``.
|
|
struct AttributeCompletion: Sendable, Equatable {
|
|
public var name: String
|
|
public var required: Bool
|
|
public var present: Bool
|
|
|
|
public init(name: String, required: Bool, present: Bool) {
|
|
self.name = name
|
|
self.required = required
|
|
self.present = present
|
|
}
|
|
}
|
|
|
|
/// Computes ``Completions`` from the compiled schema: the follow-set of a
|
|
/// content model and the resolution of an element's declared type along a
|
|
/// coding path.
|
|
enum CompletionEngine {
|
|
/// The completions for `element` given its declared `type`, resolving
|
|
/// references through `types`.
|
|
static func completions(for element: PureXML.Model.Element, type: ElementType, types: [String: ElementType]) -> Completions {
|
|
guard case let .complex(complex) = resolve(type, types: types) else {
|
|
return Completions(elements: [], complete: true, allowsAnyElement: false, attributes: [])
|
|
}
|
|
let present = Set(element.attributes.map(\.name.localName))
|
|
let attributes = complex.attributes.map {
|
|
AttributeCompletion(name: $0.name.localName, required: $0.required, present: present.contains($0.name.localName))
|
|
}
|
|
let childNames = element.children.compactMap { child -> PureXML.Model.QualifiedName? in
|
|
if case let .element(inner) = child { return inner.name }
|
|
return nil
|
|
}
|
|
switch complex.content {
|
|
case let .elementOnly(particle), let .mixed(particle):
|
|
let (labels, complete) = ContentNFABuilder.build(particle).follow(after: childNames)
|
|
let names = labels.compactMap { if case let .name(name) = $0 { name.description } else { nil } }
|
|
let anyElement = labels.contains { if case .wildcard = $0 { true } else { false } }
|
|
return Completions(elements: names, complete: complete, allowsAnyElement: anyElement, attributes: attributes)
|
|
case .empty, .simpleContent:
|
|
return Completions(elements: [], complete: true, allowsAnyElement: false, attributes: attributes)
|
|
}
|
|
}
|
|
|
|
/// Resolves the declared type of the element a coding path addresses, by
|
|
/// walking the global declaration then each parent's content model.
|
|
static func elementType(
|
|
at path: [PureXML.Validation.PathKey],
|
|
elements: [String: ElementType],
|
|
types: [String: ElementType],
|
|
) -> ElementType? {
|
|
guard let first = path.first, var current = elements[first.stringValue] else { return nil }
|
|
for step in path.dropFirst() {
|
|
guard case let .complex(complex) = resolve(current, types: types),
|
|
let childType = childTypes(of: complex)[step.stringValue]
|
|
else {
|
|
return nil
|
|
}
|
|
current = childType
|
|
}
|
|
return current
|
|
}
|
|
|
|
/// Resolves an `ElementType` to its underlying simple or complex type,
|
|
/// following named references.
|
|
static func resolve(_ type: ElementType, types: [String: ElementType]) -> ElementType {
|
|
// Completions are best-effort: an unknown or circular chain keeps the
|
|
// unresolved reference, which simply offers no completions.
|
|
if case let .resolved(resolved) = PureXML.Schema.ComplexValidator.resolveReference(type, in: types) {
|
|
return resolved
|
|
}
|
|
return type
|
|
}
|
|
|
|
/// The child element declarations of a complex type's content model, keyed
|
|
/// by the qualified-name description used in coding paths.
|
|
private static func childTypes(of complex: ComplexType) -> [String: ElementType] {
|
|
switch complex.content {
|
|
case let .elementOnly(particle), let .mixed(particle):
|
|
var result: [String: ElementType] = [:]
|
|
collect(particle.term, into: &result)
|
|
return result
|
|
case .empty, .simpleContent:
|
|
return [:]
|
|
}
|
|
}
|
|
|
|
private static func collect(_ term: Term, into result: inout [String: ElementType]) {
|
|
switch term {
|
|
case let .element(name, type):
|
|
if let type { result[name.description] = type }
|
|
case let .group(group):
|
|
for member in group.particles {
|
|
collect(member.term, into: &result)
|
|
}
|
|
case .wildcard:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|