PureYAML/Tests/StreamParsingTests.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
11 KiB
Swift

@testable import PureYAML
import Testing
@Suite("Stream Parsing")
struct StreamParsingTests {
@Test("Parses one implicit document as an indexed stream")
func test_parseStreamReturnsOneImplicitDocument() throws {
let documents = try PureYAML.parseStream(
"""
title: Example
active: true
""",
)
#expect(documents == [
.init(index: 0, value: .mapping(.init([
.init(key: "title", value: .string("Example")),
.init(key: "active", value: .bool(true)),
]))),
])
#expect(documents.map(\.index) == [0])
#expect(!documents.contains(.init(index: 0, value: .string("---"))))
}
@Test("Parses empty explicit documents as null values")
func test_emptyExplicitDocumentsBecomeNullValues() throws {
let documents = try PureYAML.parseStream(
"""
# A document may be null.
---
---
empty:
canonical: ~
---
sparse:
- ~
-
- value
""",
)
#expect(documents == [
.init(index: 0, value: .null),
.init(index: 1, value: .mapping(.init([
.init(key: "empty", value: .null),
.init(key: "canonical", value: .null),
]))),
.init(index: 2, value: .mapping(.init([
.init(key: "sparse", value: .sequence([
.null,
.null,
.string("value"),
])),
]))),
])
#expect(documents.count == 3)
#expect(documents[0].value != .string(""))
}
@Test("Parses multiple sequence documents")
func test_multipleSequenceDocuments() throws {
let documents = try PureYAML.parseStream(
"""
# Ranking of 1998 home runs
---
- Mark McGwire
- Sammy Sosa
- Ken Griffey
# Team ranking
---
- Chicago Cubs
- St Louis Cardinals
""",
)
#expect(documents == [
.init(index: 0, value: .sequence([
.string("Mark McGwire"),
.string("Sammy Sosa"),
.string("Ken Griffey"),
])),
.init(index: 1, value: .sequence([
.string("Chicago Cubs"),
.string("St Louis Cardinals"),
])),
])
#expect(documents.map(\.index) == [0, 1])
#expect(!documents.map(\.value).contains(.string("# Team ranking")))
}
@Test("Parses document end markers trailing comments and following starts")
func test_documentEndMarkersTrailingCommentsAndFollowingStarts() throws {
let documents = try PureYAML.parseStream(
"""
---
time: "20:03:20"
player: Sammy Sosa
action: strike (miss)
...
# separator comment
---
time: "20:03:47"
player: Sammy Sosa
action: grand slam
...
""",
)
#expect(documents == [
.init(index: 0, value: .mapping(.init([
.init(key: "time", value: .string("20:03:20")),
.init(key: "player", value: .string("Sammy Sosa")),
.init(key: "action", value: .string("strike (miss)")),
]))),
.init(index: 1, value: .mapping(.init([
.init(key: "time", value: .string("20:03:47")),
.init(key: "player", value: .string("Sammy Sosa")),
.init(key: "action", value: .string("grand slam")),
]))),
])
#expect(!documents.map(\.value).contains(.string("separator comment")))
}
@Test("Single document parser accepts one explicit empty document")
func test_singleDocumentParserAcceptsOneExplicitEmptyDocument() throws {
#expect(try PureYAML.parse("---") == .null)
#expect(try PureYAML.parse("---\n...\n# trailing comment") == .null)
}
@Test("Single document parser rejects multiple documents exactly")
func test_singleDocumentParserRejectsMultipleDocuments() {
expectParseError(
"""
document 1
---
document 2
""",
.unsupportedMultiDocumentStream(line: 2),
)
}
@Test("Stream parser begins a new bare document after an explicit document end")
func test_streamParserBeginsNewBareDocumentAfterExplicitDocumentEnd() throws {
// A `...` end marker closes the current document; content that follows
// without a `---` starts a fresh bare document, matching YAML test-suite
// case 7Z25.
let documents = try PureYAML.parseStream(
"""
name: First
...
name: Second
""",
)
#expect(documents == [
.init(index: 0, value: .mapping(.init([
.init(key: "name", value: .string("First")),
]))),
.init(index: 1, value: .mapping(.init([
.init(key: "name", value: .string("Second")),
]))),
])
}
@Test("Stream parser rejects extra root content after a completed node")
func test_streamParserRejectsExtraRootContentWithoutDocumentStart() {
// A flow node closes the document's single root node, so a bare scalar on
// the next line is extra content with no document boundary between it and
// the flow node. (Two bare plain lines instead fold into one scalar; see
// ParsingMultilineScalarTests.test_rootMultilinePlainScalarFolds.)
expectStreamParseError(
"""
[1, 2]
second
""",
.malformed(
reason: "did not find expected <document start>",
line: 2,
column: 1,
),
description: "did not find expected <document start> at line 2, column 1",
)
}
@Test("Stream parser rejects aliases that cross document boundaries")
func test_streamParserRejectsCrossDocumentAliases() {
expectStreamParseError(
"""
---
value: &shared first
---
copy: *shared
""",
.undefinedAlias(anchor: "shared", line: 4, column: 7),
description: "undefined alias 'shared' at line 4, column 7",
)
}
}
@Suite("Stream Validation")
struct StreamValidationTests {
@Test("Validates stream documents with exact document-indexed diagnostics")
func test_streamValidationPreservesDocumentIndexes() throws {
let documents = try PureYAML.parseStream(
"""
---
title: First
title: Second
---
routes:
- name: Users
name: People
""",
)
let expectedIssues = [
PureYAML.Stream.Issue(
documentIndex: 0,
issue: .init(
severity: .error,
reason: "Duplicate mapping key 'title'",
path: .init([.key("title")]),
),
),
PureYAML.Stream.Issue(
documentIndex: 1,
issue: .init(
severity: .error,
reason: "Duplicate mapping key 'name'",
path: .init([.key("routes"), .index(0), .key("name")]),
),
),
]
let result = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(documents)
#expect(result.issues == expectedIssues)
#expect(result.errors == expectedIssues)
#expect(result.warnings.isEmpty)
#expect(!result.isValid)
expectStreamValidationError(documents) { collection in
#expect(collection.issues == expectedIssues)
#expect(collection.description == """
document[0]: Duplicate mapping key 'title' at path: $.title
document[1]: Duplicate mapping key 'name' at path: $.routes[0].name
""")
}
}
@Test("Valid stream documents produce no diagnostics")
func test_validStreamDocumentsProduceNoDiagnostics() throws {
let documents = try PureYAML.parseStream(
"""
---
title: First
---
- one
- two
""",
)
let result = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(documents)
#expect(result == PureYAML.Stream.Result())
#expect(result.isValid)
#expect(result.errors.isEmpty)
#expect(result.warnings.isEmpty)
#expect(try PureYAML.validate(documents).isEmpty)
}
@Test("Stream validation preserves strict and non-strict warning behavior")
func test_streamValidationStrictAndNonStrictWarnings() throws {
let documents = [
PureYAML.Stream.Document(index: 4, value: .string("legacy")),
PureYAML.Stream.Document(index: 5, value: .string("modern")),
]
let validator = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>.blank
.validating(legacyModeRule(severity: .warning))
let expectedIssues = [
PureYAML.Stream.Issue(
documentIndex: 4,
issue: .init(
severity: .warning,
reason: "Legacy mode is not allowed",
path: .root,
),
),
]
expectStreamValidationError(documents, using: validator) { collection in
#expect(collection.issues == expectedIssues)
}
let warnings = try PureYAML.validate(documents, using: validator, strict: false)
#expect(warnings == expectedIssues)
let modernOnly = [PureYAML.Stream.Document(index: 6, value: .string("modern"))]
#expect(PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>.blank
.validating(legacyModeRule(severity: .warning))
.collect(modernOnly)
.issues
.isEmpty)
}
}
private func expectStreamParseError(
_ yaml: String,
_ expected: PureYAML.Parsing.ParseError,
description: String,
) {
do {
_ = try PureYAML.parseStream(yaml)
recordIssue("expected error \(expected)")
} catch let error as PureYAML.Parsing.ParseError {
#expect(error == expected)
#expect(error.description == description)
} catch {
recordIssue("unexpected error \(error)")
}
}
private func expectStreamValidationError(
_ documents: [PureYAML.Stream.Document],
using validator: PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value> = .init(),
check: (PureYAML.Stream.Issue.Collection) -> Void,
) {
do {
try PureYAML.validate(documents, using: validator)
recordIssue("expected stream validation failure")
} catch let collection as PureYAML.Stream.Issue.Collection {
check(collection)
} catch {
recordIssue("unexpected error \(error)")
}
}