@testable import PureXML import Testing /// Per-rule isolation tests in the OpenAPIKit style: each builtin validation is /// installed alone on a `blank` validator, run against a seeded subject, and the /// resulting errors are asserted by count, reason, and coding path. @Suite("Validation rules in isolation") struct ValidationRuleTests { private typealias DTDSchema = PureXML.Validation.DTDSchema private typealias XSDContext = PureXML.Validation.XSDContext private func dtd(_ xml: String) throws -> (PureXML.Model.Node, DTDSchema) { let parsed = try PureXML.Parsing.Parser().parseWithDocumentType(xml, limits: .init(allowDoctype: true)) return (parsed.node, DTDSchema(parsed.documentType)) } private func path(_ error: PureXML.Validation.ValidationError) -> [String] { error.codingPath.map(\.stringValue) } // MARK: Structural @Test("Structural.uniqueAttributes reports each duplicate at the element path") func test_uniqueAttributes() { let element = PureXML.Model.Element("a", attributes: [.init("x", "1"), .init("x", "2")]) let node = PureXML.Model.Node.document([.element(element)]) let errors = PureXML.Validation.Validator.blank .validating(PureXML.Validation.Structural.uniqueAttributes) .errors(for: node, in: ()) #expect(errors.count == 1) #expect(errors.first?.reason == "Duplicate attribute 'x' on ") #expect(path(errors[0]) == ["a"]) } // MARK: DTD @Test("DTD.contentModel reports an EMPTY element with content at the element path") func test_contentModel() throws { let (node, schema) = try dtd("]>") let errors = PureXML.Validation.Validator.blank .validating(PureXML.Validation.DTD.contentModel) .errors(for: node, in: schema) #expect(errors.count == 1) #expect(errors.first?.reason == "element is declared EMPTY but has content") #expect(path(errors[0]) == ["r"]) } @Test("DTD.attributeDeclarations reports a missing required attribute") func test_attributeDeclarations() throws { let (node, schema) = try dtd("]>") let errors = PureXML.Validation.Validator.blank .validating(PureXML.Validation.DTD.attributeDeclarations) .errors(for: node, in: schema) #expect(errors.count == 1) #expect(errors.first?.reason == "required attribute 'id' is missing on ") #expect(path(errors[0]) == ["r"]) } @Test("DTD.undeclaredElement uses the single-error Bool form (Failed to satisfy)") func test_undeclaredElement() throws { let (node, schema) = try dtd("]>") let errors = PureXML.Validation.Validator.blank .validating(PureXML.Validation.DTD.undeclaredElement) .errors(for: node, in: schema) #expect(errors.count == 1) #expect(errors.first?.reason == "Failed to satisfy: Element is declared in the DTD") #expect(path(errors[0]) == ["r"]) } @Test("DTD.identifierIntegrity reports a duplicate ID at the document root") func test_identifierIntegrity() throws { let (node, schema) = try dtd("]>") let errors = PureXML.Validation.Validator.blank .validating(PureXML.Validation.DTD.identifierIntegrity) .errors(for: node, in: schema) #expect(errors.count == 1) #expect(errors.first?.reason == "duplicate ID 'x' (declared 2 times)") // Runs once over the whole tree, so it is located at the document root. #expect(path(errors[0]).isEmpty) #expect(String(describing: errors[0]).hasSuffix("at root of document")) } // MARK: XSD @Test("XSD.contentValidity locates a simple-type violation at the root element") func test_xsdContentValidity() throws { let xsd = """ """ let compiled = try PureXML.Schema.XSDParser.parse(xsd) let node = try PureXML.parse("x") guard case let .document(children) = node, let root = children.compactMap(\.element).first else { Issue.record("no root element") return } let context = XSDContext( types: compiled.types, constraints: compiled.constraints, rootDeclaration: compiled.elements["n"], ) // The validator is rooted at the document element, as Schema.Document does. let errors = PureXML.Validation.Validator.blank .validating(PureXML.Validation.XSD.contentValidity) .errors(for: .element(root), in: context) #expect(errors.count == 1) #expect(errors.first?.reason.contains("n") == true) #expect(errors.first.map(path) == ["n"]) } }