PureYAML/Tests/TaggedCompatibilityTests.swift
Mihaela Mihaljevic bf2dc4ece7 refactor(validation): rename framework to match the validation idiom
The validation module already implemented the composable-rule idiom under
its own names (Check/Issue/SubjectContext) and a value-type Validator with
duplicated traversal machinery per layer. Renamed the core types to the
canonical names (Validation/ValidationError/ValidationErrorCollection/
ValidationContext/BuiltinValidation/AnyValidation), converted Validator to
a reference-type class with the two-tier default-rule split, and genericized
it over both Subject and Document via a new Traversable protocol so the
model and tagged layers share one walk engine instead of two copies. Wired
the JSON-representability rule set into the same shape and closed the two
missing pieces (unwrap combinator, JSON coverage/config-pin tests).
2026-07-12 18:10:00 +02:00

333 lines
13 KiB
Swift

@testable import PureYAML
import Testing
@Suite("Tagged Compatibility")
struct TaggedCompatibilityTests {
@Test("Parses explicit scalar and collection tags into a tagged node tree")
func test_parsesExplicitScalarAndCollectionTags() throws {
let root = try requireTaggedMapping(PureYAML.parseTagged(
"""
root: !<tag:example.com,2026:root> {items: !!seq [!!str true, !Thing false], meta: !!map {enabled: !!bool "false"}}
""",
))
let taggedRoot = try requireTaggedMapping(root?["root"], "expected custom-tagged root mapping")
let items = try requireTaggedSequence(taggedRoot?["items"], "expected tagged items sequence")
let meta = try requireTaggedMapping(taggedRoot?["meta"], "expected tagged meta mapping")
let enabled = try requireTaggedScalar(meta?["enabled"], "expected tagged enabled scalar")
let firstItem = try requireTaggedScalar(items?.values.first, "expected first tagged item")
let secondItem = try requireTaggedScalar(items?.values.dropFirst().first, "expected second tagged item")
#expect(taggedRoot?.tag == .init("tag:example.com,2026:root"))
#expect(items?.tag == .sequence)
#expect(meta?.tag == .mapping)
#expect(enabled?.tag == .bool)
#expect(enabled?.value == .bool(false))
#expect(firstItem?.tag == .string)
#expect(firstItem?.value == .string("true"))
#expect(secondItem?.tag == .init("!Thing"))
// The `!Thing` local tag is preserved, and it excludes the scalar from implicit resolution,
// so its value stays the string `false` rather than being read as a boolean.
#expect(secondItem?.value == .string("false"))
#expect(PureYAML.Tagged.Validator().collect(.mapping(root ?? .init(pairs: []))) == .init())
}
@Test("Model parsing keeps unsupported Foundation-backed tags as plain value trees")
func test_modelParsingKeepsFoundationBackedTagsAsPlainValues() throws {
let root = try requireMapping(PureYAML.parse(
"""
date: !!timestamp 2001-01-23
payload: !!binary |
YWJj
""",
))
#expect(root?["date"] == .string("2001-01-23"))
#expect(root?["payload"] == .string("YWJj\n"))
}
}
@Suite("Tagged Validation Compatibility")
struct TaggedValidationCompatibilityTests {
@Test("Validates unsupported built-in tags exactly")
func test_validatesUnsupportedBuiltInTagsExactly() throws {
let node = try PureYAML.parseTagged(
"""
date: !!timestamp 2001-01-23
payload: !!binary |
YWJj
set: !!set {a: null}
omap: !!omap [{a: 1}, {b: 2}]
pairs: !!pairs [{a: 1}]
""",
)
let expectedIssues = [
PureYAML.Validation.ValidationError(
severity: .error,
reason: "Unsupported built-in tag 'tag:yaml.org,2002:timestamp'",
path: .init([.key("date")]),
),
PureYAML.Validation.ValidationError(
severity: .error,
reason: "Unsupported built-in tag 'tag:yaml.org,2002:binary'",
path: .init([.key("payload")]),
),
PureYAML.Validation.ValidationError(
severity: .error,
reason: "Unsupported built-in tag 'tag:yaml.org,2002:set'",
path: .init([.key("set")]),
),
PureYAML.Validation.ValidationError(
severity: .error,
reason: "Unsupported built-in tag 'tag:yaml.org,2002:omap'",
path: .init([.key("omap")]),
),
PureYAML.Validation.ValidationError(
severity: .error,
reason: "Unsupported built-in tag 'tag:yaml.org,2002:pairs'",
path: .init([.key("pairs")]),
),
]
let result = PureYAML.Tagged.Validator().collect(node)
#expect(result.values == expectedIssues)
#expect(result.errors == expectedIssues)
expectTaggedValidationError(node) { collection in
#expect(collection.values == expectedIssues)
#expect(collection.description == expectedIssues.map(\.description).joined(separator: "\n"))
}
}
@Test("Validates built-in tags applied to the wrong node kind exactly")
func test_validatesWrongKindBuiltInTagsExactly() throws {
let node = try PureYAML.parseTagged(
"""
seq: !!seq {a: 1}
map: !!map [a]
scalar: !!str [a]
""",
)
let expectedIssues = [
PureYAML.Validation.ValidationError(
severity: .error,
reason: "Tag 'tag:yaml.org,2002:seq' expects a sequence node, found mapping",
path: .init([.key("seq")]),
),
PureYAML.Validation.ValidationError(
severity: .error,
reason: "Tag 'tag:yaml.org,2002:map' expects a mapping node, found sequence",
path: .init([.key("map")]),
),
PureYAML.Validation.ValidationError(
severity: .error,
reason: "Tag 'tag:yaml.org,2002:str' expects a scalar node, found sequence",
path: .init([.key("scalar")]),
),
]
#expect(PureYAML.Tagged.Validator().collect(node).values == expectedIssues)
}
@Test("Validates unsupported and inconsistent mapping key tags exactly")
func test_validatesMappingKeyTagsExactly() throws {
let node = try PureYAML.parseTagged(
"""
? !!timestamp 2001-01-23
: value
? !!seq key
: value
? !!merge not-merge
: value
? !!merge <<
: value
""",
)
let expectedIssues = [
PureYAML.Validation.ValidationError(
severity: .error,
reason: "Unsupported built-in tag 'tag:yaml.org,2002:timestamp' on mapping key",
path: .init([.key("2001-01-23")]),
),
PureYAML.Validation.ValidationError(
severity: .error,
reason: "Tag 'tag:yaml.org,2002:seq' expects a scalar mapping key",
path: .init([.key("key")]),
),
PureYAML.Validation.ValidationError(
severity: .error,
reason: "Tag 'tag:yaml.org,2002:merge' is only supported on '<<' mapping keys",
path: .init([.key("not-merge")]),
),
]
let mapping = try requireTaggedMapping(node)
#expect(mapping?.pairs.map(\.key) == ["2001-01-23", "key", "not-merge", "<<"])
#expect(PureYAML.Tagged.Validator().collect(node).values == expectedIssues)
}
@Test("Validates mapping node tags and mapping key tags in one deterministic pass")
func test_validatesMappingNodeAndKeyTagsTogether() {
let node = PureYAML.Tagged.Node.mapping(.init(
pairs: [
.init(
key: "2001-01-23",
keyTag: .timestamp,
value: .scalar(.init(rawValue: "value", value: .string("value"))),
),
],
tag: .set,
))
let expectedIssues = [
PureYAML.Validation.ValidationError(
severity: .error,
reason: "Unsupported built-in tag 'tag:yaml.org,2002:timestamp' on mapping key",
path: .init([.key("2001-01-23")]),
),
PureYAML.Validation.ValidationError(
severity: .error,
reason: "Unsupported built-in tag 'tag:yaml.org,2002:set'",
path: .root,
),
]
#expect(PureYAML.Tagged.Validator().collect(node).values == expectedIssues)
}
@Test("Allows project-specific tags for application validation")
func test_allowsProjectSpecificTagsForApplicationValidation() throws {
let node = try PureYAML.parseTagged(
"""
thing: !<tag:example.com,2026:thing> {name: Example}
local: !Local [one, two]
""",
)
let root = try requireTaggedMapping(node)
let thing = try requireTaggedMapping(root?["thing"], "expected custom thing mapping")
let local = try requireTaggedSequence(root?["local"], "expected local tagged sequence")
#expect(thing?.tag == .init("tag:example.com,2026:thing"))
#expect(local?.tag == .init("!Local"))
#expect(PureYAML.Tagged.Validator().collect(node) == .init())
}
@Test("Custom tagged validation rules inspect application tags exactly")
func test_customTaggedValidationRulesInspectApplicationTagsExactly() throws {
let node = try PureYAML.parseTagged("value: !Local one")
let localTagsAreWarnings = PureYAML.Validation.Validation<PureYAML.Tagged.Node, PureYAML.Tagged.Node>(
description: "Local tags are warnings",
) { context in
guard context.subject.tag == .init("!Local") else {
return []
}
return [
PureYAML.Validation.ValidationError(
severity: .warning,
reason: "Local application tag requires caller handling",
path: context.path,
),
]
}
let expectedIssues = [
PureYAML.Validation.ValidationError(
severity: .warning,
reason: "Local application tag requires caller handling",
path: .init([.key("value")]),
),
]
let validator = PureYAML.Tagged.Validator.blank.validating(localTagsAreWarnings)
#expect(PureYAML.Tagged.Validator.blank.collect(node) == .init())
#expect(validator.collect(node).values == expectedIssues)
#expect(try validator.validate(node, strict: false) == expectedIssues)
expectTaggedValidationError(node, using: validator) { collection in
#expect(collection.values == expectedIssues)
}
}
@Test("Validates tagged streams with document-indexed diagnostics")
func test_validatesTaggedStreamsWithDocumentIndexedDiagnostics() throws {
let documents = try PureYAML.parseTaggedStream(
"""
---
value: !!str true
---
value: !!timestamp 2001-01-23
""",
)
let expectedIssues = [
PureYAML.Stream.Issue(
documentIndex: 1,
issue: .init(
severity: .error,
reason: "Unsupported built-in tag 'tag:yaml.org,2002:timestamp'",
path: .init([.key("value")]),
),
),
]
#expect(documents.map(\.index) == [0, 1])
#expect(PureYAML.Tagged.Validator().collect(documents).issues == expectedIssues)
}
@Test("Tagged parsing reports invalid explicit scalar tags exactly")
func test_taggedParsingReportsInvalidExplicitScalarTagsExactly() {
expectError(
PureYAML.Parsing.ParseError.invalidTaggedScalar(
tag: "tag:yaml.org,2002:int",
value: "nope",
line: 1,
column: 8,
),
) {
_ = try PureYAML.parseTagged("value: !!int nope")
}
}
}
private func requireTaggedMapping(
_ node: PureYAML.Tagged.Node?,
_ message: String = "expected tagged mapping",
) throws -> PureYAML.Tagged.Mapping? {
guard case let .mapping(mapping)? = node else {
recordIssue(message)
return nil
}
return mapping
}
private func requireTaggedSequence(
_ node: PureYAML.Tagged.Node?,
_ message: String = "expected tagged sequence",
) throws -> PureYAML.Tagged.Sequence? {
guard case let .sequence(sequence)? = node else {
recordIssue(message)
return nil
}
return sequence
}
private func requireTaggedScalar(
_ node: PureYAML.Tagged.Node?,
_ message: String = "expected tagged scalar",
) throws -> PureYAML.Tagged.Scalar? {
guard case let .scalar(scalar)? = node else {
recordIssue(message)
return nil
}
return scalar
}
private func expectTaggedValidationError(
_ node: PureYAML.Tagged.Node,
using validator: PureYAML.Tagged.Validator = .init(),
body: (PureYAML.Validation.ValidationErrorCollection) -> Void,
) {
do {
try validator.validate(node)
recordIssue("expected tagged validation to fail")
} catch let collection as PureYAML.Validation.ValidationErrorCollection {
body(collection)
} catch {
recordIssue("expected validation issue collection, got \(error)")
}
}