feat(emitting): dump YAML streams
This commit is contained in:
parent
91a2c24901
commit
906f0c454d
@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Add `PureYAML.Stream.Document`, document-indexed stream validation issues, and
|
||||
stream validation APIs that preserve document indexes without changing
|
||||
document-local validation paths.
|
||||
- Add stream dumping for `[PureYAML.Stream.Document]` with deterministic
|
||||
explicit document starts and existing emitter option support.
|
||||
- Add merge-key expansion for plain `<<` and explicit `!!merge` keys, including
|
||||
scalar merge mappings, sequence-of-mapping merge values, local override
|
||||
behavior, and exact errors for invalid merge values.
|
||||
|
||||
@ -47,9 +47,10 @@ common scalars, quoted strings, comments, flow collections, literal and folded
|
||||
block scalars, anchors, aliases, YAML directives, document markers, explicit
|
||||
built-in scalar tags, merge-key expansion, complex mapping keys,
|
||||
multi-document stream parsing, and a matching dumper with block and flow output
|
||||
policies. It also includes path-aware validation for structural YAML checks such
|
||||
as duplicate mapping keys, plus stream validation that preserves document
|
||||
indexes. Callers that need explicit tag metadata can use `PureYAML.parseTagged(_:)` or
|
||||
policies, including multi-document stream dumping with explicit document starts.
|
||||
It also includes path-aware validation for structural YAML checks such as
|
||||
duplicate mapping keys, plus stream validation that preserves document indexes.
|
||||
Callers that need explicit tag metadata can use `PureYAML.parseTagged(_:)` or
|
||||
`PureYAML.parseTaggedStream(_:)` and run tagged validation over the preserved
|
||||
source-shaped node tree.
|
||||
|
||||
@ -137,6 +138,7 @@ title: First
|
||||
""")
|
||||
|
||||
try PureYAML.validate(documents)
|
||||
let streamYAML = PureYAML.dump(documents)
|
||||
```
|
||||
|
||||
Typed conversion is available for scalar values, keyed mapping-backed structs,
|
||||
|
||||
@ -17,6 +17,14 @@ public extension PureYAML {
|
||||
Emitting.Dumper(options: options).dump(value)
|
||||
}
|
||||
|
||||
/// Serializes indexed YAML stream documents with explicit document starts.
|
||||
static func dump(
|
||||
_ documents: [Stream.Document],
|
||||
options: Emitting.Options = .default,
|
||||
) -> String {
|
||||
Emitting.Dumper(options: options).dump(documents)
|
||||
}
|
||||
|
||||
/// Validates a parsed YAML value with the default validation rules.
|
||||
@discardableResult
|
||||
static func validate(
|
||||
|
||||
10
Sources/Emitting/Dumper.Stream.swift
Normal file
10
Sources/Emitting/Dumper.Stream.swift
Normal file
@ -0,0 +1,10 @@
|
||||
public extension PureYAML.Emitting.Dumper {
|
||||
/// Serializes indexed YAML stream documents with explicit document starts.
|
||||
func dump(_ documents: [PureYAML.Stream.Document]) -> String {
|
||||
documents
|
||||
.map { document in
|
||||
"---\n" + dump(document.value)
|
||||
}
|
||||
.joined()
|
||||
}
|
||||
}
|
||||
@ -91,6 +91,14 @@ struct DocumentationExampleTests {
|
||||
])),
|
||||
])
|
||||
#expect(try PureYAML.validate(documents).isEmpty)
|
||||
#expect(PureYAML.dump(documents) == """
|
||||
---
|
||||
title: "First"
|
||||
---
|
||||
- "Swift"
|
||||
- "YAML"
|
||||
|
||||
""")
|
||||
}
|
||||
|
||||
@Test("README tagged parsing example stays executable")
|
||||
|
||||
179
Tests/StreamDumpingTests.swift
Normal file
179
Tests/StreamDumpingTests.swift
Normal file
@ -0,0 +1,179 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
@Suite("Stream Dumping")
|
||||
struct StreamDumpingTests {
|
||||
@Test("Dumps stream documents with explicit starts exactly")
|
||||
func test_dumpsStreamDocumentsWithExplicitStartsExactly() throws {
|
||||
let documents = [
|
||||
PureYAML.Stream.Document(index: 4, value: .mapping(.init([
|
||||
.init(key: "title", value: .string("First")),
|
||||
]))),
|
||||
PureYAML.Stream.Document(index: 2, value: .sequence([
|
||||
.string("Swift"),
|
||||
.string("YAML"),
|
||||
])),
|
||||
]
|
||||
let yaml = PureYAML.dump(documents)
|
||||
|
||||
#expect(yaml == """
|
||||
---
|
||||
title: "First"
|
||||
---
|
||||
- "Swift"
|
||||
- "YAML"
|
||||
|
||||
""")
|
||||
#expect(yaml.hasPrefix("---\n"))
|
||||
#expect(documentStartCount(in: yaml) == 2)
|
||||
#expect(!yaml.contains("document[4]"))
|
||||
#expect(try PureYAML.parseStream(yaml).map(\.value) == documents.map(\.value))
|
||||
#expect(try PureYAML.validate(PureYAML.parseStream(yaml)).isEmpty)
|
||||
}
|
||||
|
||||
@Test("Dumps stream documents in array order rather than index order")
|
||||
func test_dumpsStreamDocumentsInArrayOrderRatherThanIndexOrder() throws {
|
||||
let documents = [
|
||||
PureYAML.Stream.Document(index: 10, value: .string("third")),
|
||||
PureYAML.Stream.Document(index: 0, value: .string("first")),
|
||||
PureYAML.Stream.Document(index: 5, value: .string("second")),
|
||||
]
|
||||
let yaml = PureYAML.dump(documents)
|
||||
|
||||
#expect(yaml == """
|
||||
---
|
||||
"third"
|
||||
---
|
||||
"first"
|
||||
---
|
||||
"second"
|
||||
|
||||
""")
|
||||
#expect(try PureYAML.parseStream(yaml).map(\.value) == [
|
||||
.string("third"),
|
||||
.string("first"),
|
||||
.string("second"),
|
||||
])
|
||||
#expect(!yaml.hasPrefix("---\n\"first\""))
|
||||
}
|
||||
|
||||
@Test("Dumps empty stream as an empty string")
|
||||
func test_dumpsEmptyStreamAsEmptyString() {
|
||||
#expect(PureYAML.dump([PureYAML.Stream.Document]()) == "")
|
||||
}
|
||||
|
||||
@Test("Dumps stream null documents exactly")
|
||||
func test_dumpsStreamNullDocumentsExactly() throws {
|
||||
let documents = [
|
||||
PureYAML.Stream.Document(index: 0, value: .null),
|
||||
PureYAML.Stream.Document(index: 1, value: .mapping(.init([
|
||||
.init(key: "empty", value: .null),
|
||||
]))),
|
||||
]
|
||||
let yaml = PureYAML.dump(documents)
|
||||
|
||||
#expect(yaml == """
|
||||
---
|
||||
null
|
||||
---
|
||||
empty: null
|
||||
|
||||
""")
|
||||
#expect(try PureYAML.parseStream(yaml) == [
|
||||
.init(index: 0, value: .null),
|
||||
.init(index: 1, value: .mapping(.init([
|
||||
.init(key: "empty", value: .null),
|
||||
]))),
|
||||
])
|
||||
}
|
||||
|
||||
@Test("Dumps stream documents with flow output")
|
||||
func test_dumpsStreamDocumentsWithFlowOutput() throws {
|
||||
let documents = [
|
||||
PureYAML.Stream.Document(index: 0, value: .mapping(.init([
|
||||
.init(key: "tags", value: .sequence([
|
||||
.string("Swift"),
|
||||
.string("YAML"),
|
||||
])),
|
||||
]))),
|
||||
PureYAML.Stream.Document(index: 1, value: .sequence([
|
||||
.int(1),
|
||||
.int(2),
|
||||
])),
|
||||
]
|
||||
let options = PureYAML.Emitting.Options(collectionStyle: .flow)
|
||||
let yaml = PureYAML.dump(documents, options: options)
|
||||
|
||||
#expect(yaml == """
|
||||
---
|
||||
{"tags": ["Swift", "YAML"]}
|
||||
---
|
||||
[1, 2]
|
||||
|
||||
""")
|
||||
#expect(!yaml.contains("\n - "))
|
||||
#expect(try PureYAML.parseStream(yaml).map(\.value) == documents.map(\.value))
|
||||
}
|
||||
|
||||
@Test("Dumps stream documents with literal block scalar output")
|
||||
func test_dumpsStreamDocumentsWithLiteralBlockScalarOutput() throws {
|
||||
let documents = [
|
||||
PureYAML.Stream.Document(index: 0, value: .mapping(.init([
|
||||
.init(key: "body", value: .string("one\ntwo\n")),
|
||||
]))),
|
||||
PureYAML.Stream.Document(index: 1, value: .string("alpha\nbeta")),
|
||||
]
|
||||
let options = PureYAML.Emitting.Options(scalarStyle: .literalBlockWhenMultiline)
|
||||
let yaml = PureYAML.dump(documents, options: options)
|
||||
|
||||
#expect(yaml == """
|
||||
---
|
||||
body: |
|
||||
one
|
||||
two
|
||||
---
|
||||
|-
|
||||
alpha
|
||||
beta
|
||||
|
||||
""")
|
||||
#expect(yaml.contains("body: |"))
|
||||
#expect(yaml.contains("|-\n alpha"))
|
||||
#expect(!yaml.contains("one\\ntwo"))
|
||||
#expect(try PureYAML.parseStream(yaml).map(\.value) == documents.map(\.value))
|
||||
}
|
||||
|
||||
@Test("Dumps stream documents with complex mapping keys")
|
||||
func test_dumpsStreamDocumentsWithComplexMappingKeys() throws {
|
||||
let value = PureYAML.Model.Value.mapping(.init([
|
||||
.init(
|
||||
keyNode: .sequence([
|
||||
.string("Detroit Tigers"),
|
||||
.string("Chicago Cubs"),
|
||||
]),
|
||||
value: .sequence([
|
||||
.string("2001-07-23"),
|
||||
]),
|
||||
),
|
||||
]))
|
||||
let documents = [
|
||||
PureYAML.Stream.Document(index: 0, value: value),
|
||||
]
|
||||
let yaml = PureYAML.dump(documents)
|
||||
|
||||
#expect(yaml == """
|
||||
---
|
||||
? ["Detroit Tigers", "Chicago Cubs"]
|
||||
:
|
||||
- "2001-07-23"
|
||||
|
||||
""")
|
||||
#expect(yaml.contains("? [\"Detroit Tigers\", \"Chicago Cubs\"]"))
|
||||
#expect(try PureYAML.parseStream(yaml).map(\.value) == [value])
|
||||
}
|
||||
}
|
||||
|
||||
private func documentStartCount(in yaml: String) -> Int {
|
||||
yaml.split(separator: "\n", omittingEmptySubsequences: false)
|
||||
.count(where: { $0 == "---" })
|
||||
}
|
||||
@ -131,6 +131,10 @@ can represent them.
|
||||
with `unsupportedMultiDocumentStream`. `PureYAML.parseStream(_:)` parses all
|
||||
documents in the stream, including empty explicit documents as `null`, and
|
||||
returns `PureYAML.Stream.Document` values with stable zero-based indexes.
|
||||
`PureYAML.dump(_:)` accepts those stream documents and emits explicit `---`
|
||||
document starts in the caller-provided array order. Each document body is
|
||||
rendered through the same dumper used for single-document values, so block,
|
||||
flow, literal scalar, and complex-key output policies stay centralized.
|
||||
|
||||
`PureYAML.parseTagged(_:)` and `PureYAML.parseTaggedStream(_:)` reuse the same
|
||||
scanner and event parser but compose `PureYAML.Tagged.Node` values that preserve
|
||||
@ -223,10 +227,9 @@ outside the library because it must own data-loss policy explicitly.
|
||||
|
||||
Compatibility should be added in small, test-backed slices:
|
||||
|
||||
1. Multi-document stream dumping.
|
||||
2. Additional built-in validation rules beyond duplicate-key behavior.
|
||||
3. Broader Codable compatibility beyond scalar, keyed, and unkeyed containers.
|
||||
4. Yams corpus comparison tests in a separate compatibility suite.
|
||||
1. Additional built-in validation rules beyond duplicate-key behavior.
|
||||
2. Broader Codable compatibility beyond scalar, keyed, and unkeyed containers.
|
||||
3. Yams corpus comparison tests in a separate compatibility suite.
|
||||
|
||||
The private `PureYAMLResearch` repository may be used to study Yams behavior, but
|
||||
the public implementation must be written in Swift and must not copy C parser
|
||||
|
||||
@ -24,6 +24,7 @@ See [../ATTRIBUTION.md](../ATTRIBUTION.md) for the project attribution.
|
||||
| Compose all YAML documents in a stream | `PureYAML.parseStream(_:)` | Returns indexed `PureYAML.Stream.Document` values. |
|
||||
| Preserve explicit YAML tags for analysis | `PureYAML.parseTagged(_:)` or `PureYAML.parseTaggedStream(_:)` | Returns tag-preserving nodes without Foundation-backed construction. |
|
||||
| Serialize a YAML tree | `PureYAML.dump(_:options:)` | Emits deterministic YAML with explicit block, flow, and scalar policies. |
|
||||
| Serialize all YAML documents in a stream | `PureYAML.dump(_:options:)` with `[PureYAML.Stream.Document]` | Emits deterministic explicit document starts and preserves array order. |
|
||||
| Decode a `Decodable` type from YAML | `PureYAML.decode(_:from:)` | Runs default validation before typed decoding. |
|
||||
| Encode an `Encodable` value to YAML | `PureYAML.encodeToYAML(_:options:)` | Encodes through `PureYAML.Model.Value`, then dumps. |
|
||||
| Validate document structure | `PureYAML.validate(_:using:strict:)` | Default rule reports duplicate mapping keys with exact paths. |
|
||||
@ -75,6 +76,7 @@ add a fixture beside the closest existing suite.
|
||||
| Merge keys | Plain `<<` and explicit `!!merge` keys expand mapping values and sequence-of-mapping values. Local keys override inherited keys; duplicate local keys remain visible to validation. | `MergeKeyCompatibilityTests`, `ParsingCompatibilityTests`, `DownstreamDocumentTests` |
|
||||
| YAML directives and document markers | `%YAML 1.2`, selected `%TAG` expansion, `---`, and `...` are supported. | `ParsingCompatibilityTests`, `StreamParsingTests` |
|
||||
| Multi-document streams | `PureYAML.parseStream(_:)` parses implicit and explicit documents, empty explicit documents as `null`, `...` document ends, and trailing comments before the next `---`. | `StreamParsingTests` |
|
||||
| Multi-document stream dumping | `PureYAML.dump(_:)` with stream documents emits explicit `---` before every document, preserves array order, and round-trips through `parseStream(_:)`. | `StreamDumpingTests` |
|
||||
| Stream validation | Stream validation preserves document indexes while keeping validation paths document-local. | `StreamParsingTests` |
|
||||
| Built-in scalar tags | `!!str`, `!!int`, `!!float`, `!!bool`, and `!!null` are applied when valid. | `ScalarCompatibilityFixtures` |
|
||||
| Tag-preserving parsing | `parseTagged(_:)` and `parseTaggedStream(_:)` preserve explicit scalar and collection tags for compatibility analysis. | `TaggedCompatibilityTests` |
|
||||
@ -121,7 +123,6 @@ explicit fallback value tree. This avoids silent compatibility drift.
|
||||
|
||||
Do not migrate a caller yet if it requires:
|
||||
|
||||
- multi-document stream dumping;
|
||||
- binary or timestamp conversion into Foundation types;
|
||||
- arbitrary `Any` load and dump APIs;
|
||||
- custom resolvers;
|
||||
|
||||
@ -47,6 +47,7 @@ Those tests are the executable contract for the short-form examples.
|
||||
| Parse a YAML stream while preserving explicit tags | `PureYAML.parseTaggedStream(_:)` | `[PureYAML.Tagged.Document]` |
|
||||
| Construct typed values from tagged nodes | `PureYAML.Tagged.Constructor` | Caller-owned output type |
|
||||
| Emit YAML from a value tree | `PureYAML.dump(_:options:)` | `String` |
|
||||
| Emit YAML from stream documents | `PureYAML.dump(_:options:)` with `[PureYAML.Stream.Document]` | `String` |
|
||||
| Validate a value tree | `PureYAML.validate(_:using:strict:)` | `[PureYAML.Validation.Issue]` or a thrown issue collection |
|
||||
| Validate stream documents | `PureYAML.validate(_:using:strict:)` with `[PureYAML.Stream.Document]` | `[PureYAML.Stream.Issue]` or a thrown stream issue collection |
|
||||
| Decode a typed value from YAML | `PureYAML.decode(_:from:)` | `Decodable` value |
|
||||
@ -90,6 +91,18 @@ title: First
|
||||
""")
|
||||
```
|
||||
|
||||
Use `PureYAML.dump(_:)` with `[PureYAML.Stream.Document]` to emit a
|
||||
multi-document stream. The dumper emits explicit `---` before every document and
|
||||
preserves the array order rather than sorting by `document.index`:
|
||||
|
||||
```swift
|
||||
let yaml = PureYAML.dump(documents)
|
||||
```
|
||||
|
||||
The same emitter options apply to each document body. Flow collections,
|
||||
literal-block scalar selection, and complex mapping key emission are delegated to
|
||||
the single-document dumper.
|
||||
|
||||
`PureYAML.parse(_:)` remains the single-document API. It accepts one implicit or
|
||||
explicit document, including an explicit empty document, and throws
|
||||
`unsupportedMultiDocumentStream` when a second document is present.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user