PureXML/Sources/XSLT/XSLTInstantiationSteps.swift
Mihaela Mihaljevic 380160cc2b refactor(validation): decompose XSD instance validation into composable rules
The two monolithic XSD content/identity rules only satisfied the OpenAPIKit
validation idiom at the wrong granularity: no per-constraint removability,
no isolable failing/near-miss tests. Split them into 32 named rules, each
finding tagged at its origin and cached per XSDContext so the walk still
runs once per document (the #101 decision against duplicating XSD's
recursive type resolution still holds). Wired the streaming path's ID/IDREF
and identity checks through the same framework instead of calling the
validators imperatively, and added the unwrapAndLookup combinator the rule
already named as required.

Also includes the mandated repo-wide swiftformat/swiftlint pass.
2026-07-12 14:12:17 +02:00

140 lines
6.0 KiB
Swift

// The step methods of the iterative XSLT evaluator (see XSLTInstantiation.swift).
// Each instruction either appends its produced items to the sink synchronously or
// pushes sub-work; the rest of the body is always pushed first so it pops last,
// keeping result-tree document order.
extension XSLTDriver {
func stepRun(_ body: [Instruction], _ cursor: Int, _ context: XSLTContext, _ sink: Sink) {
guard cursor < body.count else { return }
let instruction = body[cursor]
switch instruction {
case let .variable(name, select, variableBody):
// A variable binds for the rest of the body; thread the updated context.
var context = context
context.variables[name] = transformer.variableValue(select, variableBody, context)
stack.append(.run(body, cursor + 1, context, sink))
return
case let .fallback(fallbackBody):
stack.append(.run(body, cursor + 1, context, sink))
stack.append(.run(fallbackBody, 0, context, sink))
return
default:
break
}
// The remainder of the body runs after this instruction's items.
stack.append(.run(body, cursor + 1, context, sink))
if runSimple(instruction, context, sink) {
return
}
if runControl(instruction, context, sink) {
return
}
if runTemplate(instruction, context, sink) {
return
}
runConstruct(instruction, context, sink)
}
private func runSimple(_ instruction: Instruction, _ context: XSLTContext, _ sink: Sink) -> Bool {
switch instruction {
case let .literalText(text):
sink.into.items.append(.node(.text(text)))
case let .valueOf(select, raw):
let value = transformer.string(select, context)
sink.into.items.append(.node(.text(raw ? PureXML.XSLT.RawText.marked(value) : value)))
case .number:
sink.into.items.append(.node(.text(transformer.numberInstruction(instruction, context))))
case let .copyOf(select):
sink.into.items.append(contentsOf: transformer.copyOf(select, context))
case let .message(terminate, messageBody):
if terminate, transformer.termination.message == nil {
transformer.termination.message = Host.text(of: transformer.instantiate(messageBody, context))
}
default:
return false
}
return true
}
private func runControl(_ instruction: Instruction, _ context: XSLTContext, _ sink: Sink) -> Bool {
switch instruction {
case let .ifInstruction(test, thenBody):
if transformer.boolean(test, context) {
stack.append(.run(thenBody, 0, context, sink))
}
case let .choose(whens, otherwise):
let branch = whens.first { transformer.boolean($0.test, context) }?.body ?? otherwise
stack.append(.run(branch, 0, context, sink))
case let .forEach(select, sorts, eachBody):
pushForEach(select, sorts, eachBody, context, sink)
default:
return false
}
return true
}
private func runTemplate(_ instruction: Instruction, _ context: XSLTContext, _ sink: Sink) -> Bool {
switch instruction {
case let .applyTemplates(select, mode, sorts, parameters):
let nodes = transformer.sorted(transformer.selectXPathNodes(select ?? "node()", context), sorts, context)
stack.append(.applyTemplates(nodes, Application(mode: mode, parameters: parameters, caller: context, sink: sink)))
case let .callTemplate(name, parameters):
pushCallTemplate(name, parameters, context, sink)
case .applyImports:
pushApplyImports(context, sink)
default:
return false
}
return true
}
private func runConstruct(_ instruction: Instruction, _ context: XSLTContext, _ sink: Sink) {
switch instruction {
case .literalElement:
pushLiteralElement(instruction, context, sink)
case .element:
pushElement(instruction, context, sink)
case let .attribute(nameTemplate, namespaceTemplate, namespaces, attributeBody):
let name = transformer.createdName(nameTemplate, namespaceTemplate, namespaces, context, isAttribute: true)
pushTextNode(.attribute(name), attributeBody, context, sink)
case let .copy(useAttributeSets, copyBody):
pushCopy(useAttributeSets, copyBody, context, sink)
case let .comment(commentBody):
pushTextNode(.comment, commentBody, context, sink)
case let .processingInstruction(nameTemplate, piBody):
pushTextNode(.processingInstruction(transformer.avt(nameTemplate, context)), piBody, context, sink)
default:
break
}
}
private func pushForEach(
_ select: String,
_ sorts: [PureXML.XSLT.Sort],
_ body: [Instruction],
_ context: XSLTContext,
_ sink: Sink,
) {
let nodes = transformer.sorted(transformer.selectXPathNodes(select, context), sorts, context)
// Reversed so the nodes' bodies pop in document order into the sink.
for (offset, xnode) in nodes.enumerated().reversed() {
guard let owner = Host.ownerNode(xnode) else { continue }
var itemContext = XSLTContext(
node: owner,
current: xnode.treeNode == nil ? xnode : nil,
position: offset + 1,
size: nodes.count,
variables: context.variables,
)
itemContext.baseURI = context.baseURI
stack.append(.run(body, 0, itemContext, sink))
}
}
private func pushTextNode(_ kind: Finish, _ body: [Instruction], _ context: XSLTContext, _ sink: Sink) {
let child = Accumulator()
stack.append(.finish(kind, child, sink.into))
stack.append(.run(body, 0, context, Sink(depth: sink.depth, into: child)))
}
}