diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1d3316..433db59 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
+- A `fixed` value on an element whose type is a complex type with `simpleContent` is
+ now compared in the simple type's value space (cvc-elt.5.2.2.1), not as a raw string
+ (#202), so `05` satisfies an `xs:int` `fixed="5"`. The complex-element fixed check on
+ both the tree and streaming paths now passes the `simpleContent` type as the value
+ type, in lockstep, as the simple-typed-element path already does.
+
- The streaming XSD validator now applies an element's `default`/`fixed` value
constraint to its simple content (#200): an empty element takes its constraint
value, and that value (not the empty string) is validated against the type
diff --git a/Sources/Schema/ComplexValidator.swift b/Sources/Schema/ComplexValidator.swift
index 13750aa..70b228a 100644
--- a/Sources/Schema/ComplexValidator.swift
+++ b/Sources/Schema/ComplexValidator.swift
@@ -82,7 +82,10 @@ public extension PureXML.Schema {
return errors + nilErrors
}
validateContent(element, type.content, at: path, namespaceBindings: namespaceBindings, into: &errors)
- errors += elementFixedErrors(element, at: path)
+ // A simpleContent fixed value is compared in its simple type's value space
+ // (cvc-elt.5.2.2.1), e.g. "05" equals an xs:int fixed "5".
+ let simpleContentType: SimpleType? = if case let .simpleContent(simple) = type.content { simple } else { nil }
+ errors += elementFixedErrors(element, valueType: simpleContentType, at: path)
return errors
}
diff --git a/Sources/Schema/ComplexValidatorStreaming.swift b/Sources/Schema/ComplexValidatorStreaming.swift
index be95c0a..6ca278c 100644
--- a/Sources/Schema/ComplexValidatorStreaming.swift
+++ b/Sources/Schema/ComplexValidatorStreaming.swift
@@ -168,7 +168,10 @@ public extension PureXML.Schema.ComplexValidator {
validateAttributes(element, complex, at: path, into: &errors)
if let nilErrors = nilErrors(element, at: path) { return errors + nilErrors }
shallowContent(element, complex.content, at: path, into: &errors)
- errors += elementFixedErrors(element, at: path)
+ // A simpleContent fixed value is compared in its simple type's value space
+ // (cvc-elt.5.2.2.1), matching the tree path's validate(_:against:).
+ let simpleContentType: PureXML.Schema.SimpleType? = if case let .simpleContent(simple) = complex.content { simple } else { nil }
+ errors += elementFixedErrors(element, valueType: simpleContentType, at: path)
case .typeReference:
// The shared resolver also guards a circular chain, which would
// otherwise recurse here without terminating.
diff --git a/Tests/SchemaSimpleContentFixedTests.swift b/Tests/SchemaSimpleContentFixedTests.swift
new file mode 100644
index 0000000..b0e61a4
--- /dev/null
+++ b/Tests/SchemaSimpleContentFixedTests.swift
@@ -0,0 +1,38 @@
+import Testing
+@testable import PureXML
+
+/// A `fixed` value on an element whose type is a complex type with `simpleContent`
+/// must be compared in the simple type's value space (cvc-elt.5.2.2.1), not as a raw
+/// string (#202). Both the tree and streaming paths must agree, so "05" satisfies an
+/// xs:int `fixed="5"` and is not falsely rejected.
+@Suite("XSD simpleContent fixed value-space (#202)")
+struct SchemaSimpleContentFixedTests {
+ private let schema = """
+
+
+
+
+
+
+
+
+ """
+
+ private func agreement(_ instance: String) throws -> [String] {
+ let document = try PureXML.Schema.Document(schema)
+ let tree = try document.validate(instance).map(\.reason).sorted()
+ let streamed = try document.validate(streaming: instance).map(\.reason).sorted()
+ #expect(tree == streamed, "streaming disagreed with tree:\n tree: \(tree)\n stream: \(streamed)")
+ return streamed
+ }
+
+ @Test("a value-space-equal fixed simpleContent value is accepted on both paths")
+ func test_valueSpaceEqualAccepted() throws {
+ #expect(try agreement("05").isEmpty)
+ }
+
+ @Test("a value-space-different fixed simpleContent value is rejected on both paths")
+ func test_valueSpaceDifferentRejected() throws {
+ #expect(try !agreement("6").isEmpty)
+ }
+}