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.
147 lines
7.2 KiB
Swift
147 lines
7.2 KiB
Swift
extension PureXML.Schema.ComplexValidator {
|
|
func validateAttributes(
|
|
_ element: PureXML.Model.Element,
|
|
_ type: PureXML.Schema.ComplexType,
|
|
at path: [PureXML.Validation.PathKey],
|
|
into errors: inout [XSDFinding],
|
|
) {
|
|
let present = element.attributes.filter { !Self.isNamespaceDeclaration($0) && !Self.isSchemaInstanceAttribute($0) }
|
|
for use in type.attributes where !use.prohibited {
|
|
let match = present.first { Self.attributeMatches($0.name, declared: use.name, use: use, on: element.name) }
|
|
if let match {
|
|
if let error = use.type.validate(match.value) {
|
|
errors.append(XSDFinding(.attributeTypeValid, reason: "attribute '\(use.name.localName)': \(error)", at: path))
|
|
}
|
|
if let fixed = use.valueConstraint?.fixedValue, !use.type.valueMatches(match.value, literal: fixed) {
|
|
errors.append(XSDFinding(.attributeFixedMatches, reason: "attribute '\(use.name.localName)' is fixed and must be '\(fixed)'", at: path))
|
|
}
|
|
recordIDs(use.type, value: match.value, at: path)
|
|
} else if use.required {
|
|
errors.append(XSDFinding(.attributeRequiredPresent, reason: "missing required attribute '\(use.name.localName)'", at: path))
|
|
} else if let constraint = use.valueConstraint {
|
|
// An absent attribute takes its `default`/`fixed` value, so a
|
|
// defaulted or fixed IDREF/IDREFS must still resolve to a matching
|
|
// ID (cvc-id): the supplied value participates in document-scoped
|
|
// ID/IDREF checking exactly as a present value would.
|
|
recordIDs(use.type, value: constraint.value, at: path)
|
|
}
|
|
}
|
|
validateWildcardAttributes(present, element: element, type: type, at: path, into: &errors)
|
|
if idTypedAttributeCount(present, type, on: element) > 1 {
|
|
errors.append(XSDFinding(
|
|
.attributeSingleID,
|
|
reason: "an element may carry at most one attribute of type ID",
|
|
at: path,
|
|
))
|
|
}
|
|
}
|
|
|
|
/// The number of `present` attributes whose effective type is `xs:ID` (an
|
|
/// element may carry at most one; XSD 1.0 §3.4.6, cvc-complex-type). An
|
|
/// attribute's type is its declared use's type when it matches one, otherwise
|
|
/// the global declaration a matching wildcard admits.
|
|
private func idTypedAttributeCount(
|
|
_ present: [PureXML.Model.Attribute],
|
|
_ type: PureXML.Schema.ComplexType,
|
|
on element: PureXML.Model.Element,
|
|
) -> Int {
|
|
present.reduce(into: 0) { count, attribute in
|
|
if isIDTyped(attribute, type, on: element) {
|
|
count += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Whether `attribute`'s effective type is `xs:ID`: the type of the declared
|
|
/// use it matches, or the global declaration a matching wildcard admits.
|
|
private func isIDTyped(
|
|
_ attribute: PureXML.Model.Attribute,
|
|
_ type: PureXML.Schema.ComplexType,
|
|
on element: PureXML.Model.Element,
|
|
) -> Bool {
|
|
if let use = type.attributes.first(where: { !$0.prohibited && Self.attributeMatches(attribute.name, declared: $0.name, use: $0, on: element.name) }) {
|
|
return use.type.isID
|
|
}
|
|
guard let wildcard = type.attributeWildcard, wildcard.admits(attribute.name),
|
|
let use = globalAttributeUse(for: attribute.name)
|
|
else { return false }
|
|
return use.type.isID
|
|
}
|
|
|
|
/// Whether an instance attribute satisfies a declared use. Chameleon-included
|
|
/// global attributes may appear unprefixed on an element in the effective TN.
|
|
static func attributeMatches(
|
|
_ instance: PureXML.Model.QualifiedName,
|
|
declared: PureXML.Model.QualifiedName,
|
|
use: PureXML.Schema.AttributeUse,
|
|
on elementName: PureXML.Model.QualifiedName,
|
|
) -> Bool {
|
|
if sameName(instance, declared) {
|
|
return true
|
|
}
|
|
guard use.chameleonUnprefixed,
|
|
instance.namespaceURI == nil || instance.namespaceURI?.isEmpty == true,
|
|
let declaredNamespace = declared.namespaceURI, !declaredNamespace.isEmpty
|
|
else { return false }
|
|
return instance.localName == declared.localName && declaredNamespace == elementName.namespaceURI
|
|
}
|
|
|
|
private func validateWildcardAttributes(
|
|
_ present: [PureXML.Model.Attribute],
|
|
element: PureXML.Model.Element,
|
|
type: PureXML.Schema.ComplexType,
|
|
at path: [PureXML.Validation.PathKey],
|
|
into errors: inout [XSDFinding],
|
|
) {
|
|
// A `use="prohibited"` use is excluded from the effective {attribute uses},
|
|
// so a prohibited attribute is treated as undeclared and left to the
|
|
// attribute wildcard (which may still admit it) or reported as undeclared.
|
|
let declared = type.attributes.filter { !$0.prohibited }
|
|
let elementName = element.name
|
|
let wildcard = type.attributeWildcard
|
|
for attribute in present where !declared.contains(where: { Self.attributeMatches(attribute.name, declared: $0.name, use: $0, on: elementName) }) {
|
|
guard let wildcard, wildcard.admits(attribute.name) else {
|
|
errors.append(XSDFinding(.attributeDeclaredOrPermitted, reason: "undeclared attribute '\(attribute.name.localName)'", at: path))
|
|
continue
|
|
}
|
|
switch wildcard.processContents {
|
|
case .skip:
|
|
continue
|
|
case .lax:
|
|
if let use = globalAttributeUse(for: attribute.name) {
|
|
validateWildcardAttribute(attribute, use: use, at: path, into: &errors)
|
|
}
|
|
case .strict:
|
|
guard let use = globalAttributeUse(for: attribute.name) else {
|
|
errors.append(XSDFinding(
|
|
.wildcardAttributeUndeclared,
|
|
reason: "no declaration for wildcard-matched attribute '\(attribute.name.localName)'",
|
|
at: path,
|
|
))
|
|
continue
|
|
}
|
|
validateWildcardAttribute(attribute, use: use, at: path, into: &errors)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func globalAttributeUse(for name: PureXML.Model.QualifiedName) -> PureXML.Schema.AttributeUse? {
|
|
globalAttributes[PureXML.Schema.XSDParser.attributeDeclarationKey(name)]
|
|
}
|
|
|
|
private func validateWildcardAttribute(
|
|
_ attribute: PureXML.Model.Attribute,
|
|
use: PureXML.Schema.AttributeUse,
|
|
at path: [PureXML.Validation.PathKey],
|
|
into errors: inout [XSDFinding],
|
|
) {
|
|
if let error = use.type.validate(attribute.value) {
|
|
errors.append(XSDFinding(.wildcardAttributeTypeValid, reason: "attribute '\(attribute.name.localName)': \(error)", at: path))
|
|
}
|
|
if let fixed = use.valueConstraint?.fixedValue, !use.type.valueMatches(attribute.value, literal: fixed) {
|
|
errors.append(XSDFinding(.wildcardAttributeFixedMatches, reason: "attribute '\(attribute.name.localName)' is fixed and must be '\(fixed)'", at: path))
|
|
}
|
|
recordIDs(use.type, value: attribute.value, at: path)
|
|
}
|
|
}
|