Completes issue #10. Canonical mode follows libyaml's canonical layout on both the Model and Tagged dump paths (tags explicit, scalars double quoted, flow one entry per line, anchors and aliases preserved on the Tagged path). explicitDocumentStart/End wrap the body in --- and ... markers. lineWidth wraps overflowing flow collections one entry per line and folds plain block scalars at single-space boundaries, vetoing any fold that would manufacture syntax, so values always reparse unchanged. Closes #10
162 lines
6.7 KiB
Swift
162 lines
6.7 KiB
Swift
@testable import PureYAML
|
|
import Testing
|
|
|
|
@Suite("Canonical mode, document markers, and line width")
|
|
struct EmitterCanonicalAndWidthTests {
|
|
@Test("Canonical form emits explicit tags, quotes, and flow entries per line")
|
|
func test_canonicalGoldenOutput() throws {
|
|
let value = try PureYAML.parse("name: config\ncount: 3\nitems: [a, b]\n")
|
|
let dumped = PureYAML.dump(value, options: .init(canonical: true))
|
|
#expect(dumped == """
|
|
---
|
|
!!map {
|
|
? !!str "name"
|
|
: !!str "config",
|
|
? !!str "count"
|
|
: !!int "3",
|
|
? !!str "items"
|
|
: !!seq [
|
|
!!str "a",
|
|
!!str "b",
|
|
],
|
|
}
|
|
|
|
""")
|
|
}
|
|
|
|
@Test("Canonical output reparses to the identical value")
|
|
func test_canonicalReparsesToSameValue() throws {
|
|
let source = """
|
|
server: web-1
|
|
replicas: 2
|
|
healthy: true
|
|
threshold: 0.75
|
|
fallback: null
|
|
tags: [prod, eu]
|
|
limits:
|
|
memory: 512
|
|
"""
|
|
let value = try PureYAML.parse(source)
|
|
let canonical = PureYAML.dump(value, options: .init(canonical: true))
|
|
let reparsed = try PureYAML.parse(canonical)
|
|
#expect(reparsed == value)
|
|
}
|
|
|
|
@Test("Canonical scalar document and empty collections")
|
|
func test_canonicalScalarAndEmpties() {
|
|
#expect(PureYAML.dump(.string("x"), options: .init(canonical: true)) == "---\n!!str \"x\"\n")
|
|
#expect(PureYAML.dump(.mapping(.init(pairs: [])), options: .init(canonical: true)) == "---\n!!map {}\n")
|
|
#expect(PureYAML.dump(.sequence([]), options: .init(canonical: true)) == "---\n!!seq []\n")
|
|
}
|
|
|
|
@Test("Canonical tagged tree keeps anchors, aliases, and carried tags")
|
|
func test_canonicalTaggedTree() throws {
|
|
let node = try PureYAML.parseTagged("base: &b !Custom widget\nref: *b\n")
|
|
let canonical = PureYAML.dump(node, options: .init(canonical: true))
|
|
#expect(canonical == """
|
|
---
|
|
!!map {
|
|
? !!str "base"
|
|
: &b !<!Custom> "widget",
|
|
? !!str "ref"
|
|
: *b,
|
|
}
|
|
|
|
""")
|
|
let reparsed = try PureYAML.parseTagged(canonical)
|
|
#expect(reparsed.modelValueErasingTags == node.modelValueErasingTags)
|
|
guard case let .mapping(mapping) = reparsed else {
|
|
Issue.record("expected mapping")
|
|
return
|
|
}
|
|
#expect(mapping["ref"]?.kind == .alias)
|
|
#expect(mapping["base"]?.tag == .init("!Custom"))
|
|
}
|
|
|
|
@Test("Explicit document markers wrap the body")
|
|
func test_explicitDocumentMarkers() throws {
|
|
let value = PureYAML.Model.Value.mapping(.init([.init(key: "a", value: .int(1))]))
|
|
let both = PureYAML.dump(value, options: .init(explicitDocumentStart: true, explicitDocumentEnd: true))
|
|
#expect(both == "---\na: 1\n...\n")
|
|
let startOnly = PureYAML.dump(value, options: .init(explicitDocumentStart: true))
|
|
#expect(startOnly == "---\na: 1\n")
|
|
let reparsed = try PureYAML.parse(both)
|
|
#expect(reparsed == value)
|
|
}
|
|
|
|
@Test("Line width folds long plain scalars and the value survives")
|
|
func test_lineWidthFoldsPlainScalars() throws {
|
|
let long = "the quick brown fox jumps over the lazy dog and keeps going well past any reasonable width"
|
|
let value = PureYAML.Model.Value.mapping(.init([.init(key: "sentence", value: .string(long))]))
|
|
let options = PureYAML.Emitting.Options(scalarStyle: .plainWhenSafe, lineWidth: 40)
|
|
let dumped = PureYAML.dump(value, options: options)
|
|
#expect(dumped.split(separator: "\n").count > 1)
|
|
#expect(dumped.split(separator: "\n").allSatisfy { $0.count <= 40 })
|
|
let reparsed = try PureYAML.parse(dumped)
|
|
#expect(reparsed == value)
|
|
}
|
|
|
|
@Test("Line width leaves quoted scalars and unbreakable words alone")
|
|
func test_lineWidthLeavesUnsafeContentAlone() {
|
|
let word = String(repeating: "x", count: 90)
|
|
let value = PureYAML.Model.Value.mapping(.init([.init(key: "blob", value: .string(word))]))
|
|
let plain = PureYAML.dump(value, options: .init(scalarStyle: .plainWhenSafe, lineWidth: 40))
|
|
#expect(plain == "blob: \(word)\n")
|
|
let quoted = PureYAML.dump(value, options: .init(scalarStyle: .quoted, lineWidth: 40))
|
|
#expect(quoted == "blob: \"\(word)\"\n")
|
|
}
|
|
|
|
@Test("A fold that would manufacture syntax is vetoed")
|
|
func test_unsafeFoldBoundariesVetoed() throws {
|
|
let risky = "watch the colon: placement and the - dash and the # mark carefully now"
|
|
let value = PureYAML.Model.Value.string(risky)
|
|
let dumped = PureYAML.dump(value, options: .init(scalarStyle: .plainWhenSafe, lineWidth: 30))
|
|
let reparsed = try PureYAML.parse(dumped)
|
|
#expect(reparsed == value)
|
|
}
|
|
|
|
@Test("Line width wraps flow collections one entry per line")
|
|
func test_lineWidthWrapsFlow() throws {
|
|
let value = try PureYAML.parse("list: [alpha, beta, gamma, delta, epsilon, zeta, eta, theta]\n")
|
|
let options = PureYAML.Emitting.Options(
|
|
scalarStyle: .plainWhenSafe,
|
|
collectionStyle: .flow,
|
|
lineWidth: 30,
|
|
)
|
|
let dumped = PureYAML.dump(value, options: options)
|
|
#expect(dumped.split(separator: "\n").count > 1)
|
|
let reparsed = try PureYAML.parse(dumped)
|
|
#expect(reparsed == value)
|
|
}
|
|
|
|
@Test("Sequence items fold with safe continuation indentation")
|
|
func test_sequenceItemFolding() throws {
|
|
let long = "one two three four five six seven eight nine ten eleven twelve thirteen"
|
|
let value = PureYAML.Model.Value.sequence([.string(long), .string("short")])
|
|
let dumped = PureYAML.dump(value, options: .init(scalarStyle: .plainWhenSafe, lineWidth: 30))
|
|
let reparsed = try PureYAML.parse(dumped)
|
|
#expect(reparsed == value)
|
|
}
|
|
|
|
@Test("Consecutive interior spaces forbid folding at that boundary")
|
|
func test_consecutiveSpacesSurviveFolding() throws {
|
|
let doubled = "left middle and then a good number of extra words to overflow the width right"
|
|
let value = PureYAML.Model.Value.string(doubled)
|
|
let dumped = PureYAML.dump(value, options: .init(scalarStyle: .plainWhenSafe, lineWidth: 25))
|
|
let reparsed = try PureYAML.parse(dumped)
|
|
#expect(reparsed == value)
|
|
}
|
|
|
|
@Test("Canonical wins over conflicting style options")
|
|
func test_canonicalOverridesStyles() throws {
|
|
let value = try PureYAML.parse("a: 1\n")
|
|
let styled = PureYAML.dump(value, options: .init(
|
|
scalarStyle: .plainWhenSafe,
|
|
collectionStyle: .flow,
|
|
canonical: true,
|
|
lineWidth: 10,
|
|
))
|
|
#expect(styled == "---\n!!map {\n ? !!str \"a\"\n : !!int \"1\",\n}\n")
|
|
}
|
|
}
|