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).
333 lines
11 KiB
Swift
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)")
|
|
}
|
|
}
|