feat(codable): broaden compatibility coverage
This commit is contained in:
parent
351af24731
commit
29ecce451a
@ -16,6 +16,7 @@ For code changes, load at minimum:
|
||||
- `docs/rules/testing.md`
|
||||
- `docs/rules/verification.md`
|
||||
- `docs/rules/commits.md`
|
||||
- `docs/rules/research-compatibility.md`
|
||||
|
||||
## What PureYAML Is
|
||||
|
||||
@ -35,6 +36,10 @@ Do not copy C code or upstream Yams source into this public package.
|
||||
Keep attribution in `ATTRIBUTION.md` accurate when compatibility work references
|
||||
Yams or CYaml behavior.
|
||||
|
||||
Compatibility work must start from that research source. Before changing parser,
|
||||
emitter, validation, or typed conversion behavior, inspect the matching
|
||||
PureYAMLResearch source or fixture and use it only to define behavior and tests.
|
||||
|
||||
## Namespace Rules
|
||||
|
||||
Every public type lives under the `PureYAML` namespace tree and mirrors its
|
||||
|
||||
10
CHANGELOG.md
10
CHANGELOG.md
@ -58,6 +58,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
nested sequences and sequence properties on keyed structs.
|
||||
- Add a fixture-backed sequence typed-conversion test corpus and a
|
||||
forbidden-pattern verification gate for package, source, and test contracts.
|
||||
- Add broader Codable compatibility coverage for dynamic mapping keys, nested
|
||||
container round trips, keyed super coders, default `super` coders, and
|
||||
deterministic unsupported-shape errors.
|
||||
- Add an explicit research-compatibility rule requiring parser, emitter,
|
||||
validation, and typed conversion work to start from PureYAMLResearch behavior
|
||||
without copying implementation code.
|
||||
|
||||
### Changed
|
||||
|
||||
@ -66,3 +72,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Harden duplicate-key validation to use set membership while preserving exact
|
||||
diagnostics.
|
||||
- Make validation rules immutable after construction.
|
||||
- Preserve keyed super-encoder field order even when delayed child encoders are
|
||||
written in a different order than they were requested.
|
||||
- Make keyed default `superEncoder()` and `superDecoder()` use the standard
|
||||
`super` mapping key instead of the current mapping.
|
||||
|
||||
24
README.md
24
README.md
@ -47,7 +47,7 @@ flowchart TB
|
||||
SuperEpic8["#8 Parser Replacement Roadmap"]:::epic
|
||||
Epic1["#1 Pure Swift Parse Core"]:::done
|
||||
Epic9["#9 Deterministic Emitter Core"]:::done
|
||||
Epic10["#10 Typed Decoding and Encoding"]:::todo
|
||||
Epic10["#10 Typed Decoding and Encoding"]:::done
|
||||
Epic11["#11 YAML Compatibility Corpus"]:::todo
|
||||
Epic12["#12 Usage and Migration Docs"]:::todo
|
||||
Epic13["#13 Release Readiness"]:::todo
|
||||
@ -61,24 +61,7 @@ flowchart TB
|
||||
|
||||
#9 Deterministic Emitter Core is complete in main.
|
||||
|
||||
#10 Typed Decoding and Encoding is active:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
classDef done fill:#ddf9e4,stroke:#34c759,color:#111827
|
||||
classDef review fill:#fff7d6,stroke:#ffcc00,color:#111827
|
||||
classDef epic fill:#f2e5ff,stroke:#af52de,color:#111827
|
||||
classDef todo fill:#f2f4f7,stroke:#8e8e93,color:#111827
|
||||
Epic10["#10 Typed Decoding and Encoding"]:::epic
|
||||
Scalar20["#20 Scalar Typed Conversion"]:::done
|
||||
Keyed21["#21 Keyed Typed Conversion"]:::done
|
||||
Unkeyed22["#22 Unkeyed Typed Conversion"]:::done
|
||||
Compat23["#23 Broader Codable Compatibility"]:::todo
|
||||
Epic10 --> Scalar20
|
||||
Scalar20 --> Keyed21
|
||||
Keyed21 --> Unkeyed22
|
||||
Unkeyed22 --> Compat23
|
||||
```
|
||||
#10 Typed Decoding and Encoding is complete in main.
|
||||
|
||||
## Status
|
||||
|
||||
@ -121,7 +104,8 @@ try PureYAML.validate(document)
|
||||
```
|
||||
|
||||
Typed conversion is available for scalar values, keyed mapping-backed structs,
|
||||
and unkeyed sequences:
|
||||
dictionary-like string mappings, unkeyed sequences, nested containers, and
|
||||
keyed super coders:
|
||||
|
||||
```swift
|
||||
struct Info: Codable {
|
||||
|
||||
18
Sources/Coding/SuperCodingKey.swift
Normal file
18
Sources/Coding/SuperCodingKey.swift
Normal file
@ -0,0 +1,18 @@
|
||||
struct SuperCodingKey: CodingKey {
|
||||
var stringValue: String
|
||||
var intValue: Int?
|
||||
|
||||
init() {
|
||||
stringValue = "super"
|
||||
intValue = nil
|
||||
}
|
||||
|
||||
init?(stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
intValue = nil
|
||||
}
|
||||
|
||||
init?(intValue _: Int) {
|
||||
nil
|
||||
}
|
||||
}
|
||||
@ -117,23 +117,18 @@ struct KeyedDecodingContainerImpl<Key: CodingKey>: KeyedDecodingContainerProtoco
|
||||
}
|
||||
|
||||
func superDecoder() throws -> any Swift.Decoder {
|
||||
PureYAML.Decoding.Decoder(
|
||||
value: .mapping(mapping),
|
||||
codingPath: codingPath,
|
||||
userInfo: userInfo,
|
||||
validatesInput: validatesInput,
|
||||
)
|
||||
try decoder(for: SuperCodingKey())
|
||||
}
|
||||
|
||||
func superDecoder(forKey key: Key) throws -> any Swift.Decoder {
|
||||
try decoder(for: key)
|
||||
}
|
||||
|
||||
private func singleValue(for key: Key) throws -> any SingleValueDecodingContainer {
|
||||
private func singleValue(for key: any CodingKey) throws -> any SingleValueDecodingContainer {
|
||||
try decoder(for: key).singleValueContainer()
|
||||
}
|
||||
|
||||
private func decoder(for key: Key) throws -> PureYAML.Decoding.Decoder {
|
||||
private func decoder(for key: any CodingKey) throws -> PureYAML.Decoding.Decoder {
|
||||
try PureYAML.Decoding.Decoder(
|
||||
value: value(for: key),
|
||||
codingPath: codingPath + [key],
|
||||
@ -142,7 +137,7 @@ struct KeyedDecodingContainerImpl<Key: CodingKey>: KeyedDecodingContainerProtoco
|
||||
)
|
||||
}
|
||||
|
||||
private func value(for key: Key) throws -> PureYAML.Model.Value {
|
||||
private func value(for key: any CodingKey) throws -> PureYAML.Model.Value {
|
||||
guard let value = mapping[key.stringValue] else {
|
||||
throw PureYAML.Decoding.Error.keyNotFound(
|
||||
key: key.stringValue,
|
||||
@ -154,7 +149,7 @@ struct KeyedDecodingContainerImpl<Key: CodingKey>: KeyedDecodingContainerProtoco
|
||||
|
||||
private func validateIfNeeded(
|
||||
_ value: PureYAML.Model.Value,
|
||||
forKey key: Key,
|
||||
forKey key: any CodingKey,
|
||||
) throws {
|
||||
if validatesInput {
|
||||
try PureYAML.Decoding.validate(value, codingPath: codingPath + [key])
|
||||
|
||||
@ -130,14 +130,21 @@ struct KeyedEncodingContainerImpl<Key: CodingKey>: KeyedEncodingContainerProtoco
|
||||
}
|
||||
|
||||
mutating func superEncoder() -> any Swift.Encoder {
|
||||
PureYAML.Encoding.Encoder(
|
||||
codingPath: codingPath,
|
||||
let key = SuperCodingKey()
|
||||
storage.set(.null, forKey: key.stringValue)
|
||||
let childStorage = PureYAML.Encoding.Encoder.Storage(
|
||||
parent: storage,
|
||||
parentKey: key.stringValue,
|
||||
)
|
||||
return PureYAML.Encoding.Encoder(
|
||||
codingPath: codingPath + [key],
|
||||
userInfo: userInfo,
|
||||
storage: storage,
|
||||
storage: childStorage,
|
||||
)
|
||||
}
|
||||
|
||||
mutating func superEncoder(forKey key: Key) -> any Swift.Encoder {
|
||||
storage.set(.null, forKey: key.stringValue)
|
||||
let childStorage = PureYAML.Encoding.Encoder.Storage(
|
||||
parent: storage,
|
||||
parentKey: key.stringValue,
|
||||
|
||||
269
Tests/CodableCompatibilityFixtures.swift
Normal file
269
Tests/CodableCompatibilityFixtures.swift
Normal file
@ -0,0 +1,269 @@
|
||||
@testable import PureYAML
|
||||
|
||||
let dynamicSettingsValue = PureYAML.Model.Value.mapping(.init([
|
||||
.init(key: "/users", value: .int(2)),
|
||||
.init(key: "name.with.dot", value: .int(3)),
|
||||
]))
|
||||
|
||||
let nestedCompatibilityValue = PureYAML.Model.Value.mapping(.init([
|
||||
.init(key: "title", value: .string("Typed YAML")),
|
||||
.init(key: "metadata", value: .mapping(.init([
|
||||
.init(key: "author", value: .string("Mihaela")),
|
||||
.init(key: "rating", value: .int(5)),
|
||||
]))),
|
||||
.init(key: "sections", value: .sequence([
|
||||
.mapping(.init([
|
||||
.init(key: "heading", value: .string("Intro")),
|
||||
.init(key: "items", value: .sequence([
|
||||
.string("one"),
|
||||
.string("two"),
|
||||
])),
|
||||
])),
|
||||
.mapping(.init([
|
||||
.init(key: "heading", value: .string("Outro")),
|
||||
.init(key: "items", value: .sequence([])),
|
||||
])),
|
||||
])),
|
||||
]))
|
||||
|
||||
let nestedCompatibilityFixture = NestedCompatibilityDocument(
|
||||
title: "Typed YAML",
|
||||
metadata: .init(author: "Mihaela", rating: 5),
|
||||
sections: [
|
||||
.init(heading: "Intro", items: ["one", "two"]),
|
||||
.init(heading: "Outro", items: []),
|
||||
],
|
||||
)
|
||||
|
||||
let codingPathProbeValue = PureYAML.Model.Value.mapping(.init([
|
||||
.init(key: "payload", value: .string("child")),
|
||||
.init(key: "metadata", value: .mapping(.init([
|
||||
.init(key: "author", value: .string("Mihaela")),
|
||||
]))),
|
||||
.init(key: "items", value: .sequence([
|
||||
.mapping(.init([
|
||||
.init(key: "author", value: .string("Nested")),
|
||||
])),
|
||||
])),
|
||||
]))
|
||||
|
||||
struct DynamicSettings: Codable, Equatable {
|
||||
struct Entry: Equatable {
|
||||
var key: String
|
||||
var value: Int
|
||||
}
|
||||
|
||||
var entries: [Entry]
|
||||
|
||||
init(entries: [Entry]) {
|
||||
self.entries = entries
|
||||
}
|
||||
|
||||
init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: DynamicCodingKey.self)
|
||||
entries = try container.allKeys.map { key in
|
||||
try Entry(
|
||||
key: key.stringValue,
|
||||
value: container.decode(Int.self, forKey: key),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to encoder: any Encoder) throws {
|
||||
var container = encoder.container(keyedBy: DynamicCodingKey.self)
|
||||
for entry in entries {
|
||||
try container.encode(
|
||||
entry.value,
|
||||
forKey: DynamicCodingKey(entry.key),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NestedCompatibilityDocument: Codable, Equatable {
|
||||
var title: String
|
||||
var metadata: NestedCompatibilityMetadata
|
||||
var sections: [NestedCompatibilitySection]
|
||||
}
|
||||
|
||||
struct NestedCompatibilityMetadata: Codable, Equatable {
|
||||
var author: String
|
||||
var rating: Int
|
||||
}
|
||||
|
||||
struct NestedCompatibilitySection: Codable, Equatable {
|
||||
var heading: String
|
||||
var items: [String]
|
||||
}
|
||||
|
||||
struct SuperCodedPayload: Codable, Equatable {
|
||||
var value: String
|
||||
}
|
||||
|
||||
struct KeyedSuperBox: Codable, Equatable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case payload
|
||||
}
|
||||
|
||||
var payload: SuperCodedPayload
|
||||
|
||||
init(payload: SuperCodedPayload) {
|
||||
self.payload = payload
|
||||
}
|
||||
|
||||
init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
payload = try SuperCodedPayload(
|
||||
from: container.superDecoder(forKey: .payload),
|
||||
)
|
||||
}
|
||||
|
||||
func encode(to encoder: any Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try payload.encode(to: container.superEncoder(forKey: .payload))
|
||||
}
|
||||
}
|
||||
|
||||
struct KeyedDefaultSuperBox: Codable, Equatable {
|
||||
var payload: SuperCodedPayload
|
||||
|
||||
init(payload: SuperCodedPayload) {
|
||||
self.payload = payload
|
||||
}
|
||||
|
||||
init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingPathProbeTopKey.self)
|
||||
let superDecoder = try container.superDecoder()
|
||||
expectCodingPath(superDecoder.codingPath, strings: ["super"], intValues: [nil])
|
||||
payload = try SuperCodedPayload(from: superDecoder)
|
||||
}
|
||||
|
||||
func encode(to encoder: any Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingPathProbeTopKey.self)
|
||||
let superEncoder = container.superEncoder()
|
||||
expectCodingPath(superEncoder.codingPath, strings: ["super"], intValues: [nil])
|
||||
try payload.encode(to: superEncoder)
|
||||
}
|
||||
}
|
||||
|
||||
struct DelayedKeyedSuperEncoders: Encodable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case first
|
||||
case second
|
||||
}
|
||||
|
||||
func encode(to encoder: any Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
let first = container.superEncoder(forKey: .first)
|
||||
let second = container.superEncoder(forKey: .second)
|
||||
try "two".encode(to: second)
|
||||
try "one".encode(to: first)
|
||||
}
|
||||
}
|
||||
|
||||
struct MissingPayloadBox: Decodable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case payload
|
||||
}
|
||||
|
||||
init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
_ = try container.superDecoder(forKey: .payload)
|
||||
}
|
||||
}
|
||||
|
||||
struct MissingDefaultSuperBox: Decodable {
|
||||
init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingPathProbeTopKey.self)
|
||||
_ = try container.superDecoder()
|
||||
}
|
||||
}
|
||||
|
||||
struct NoValueEncoded: Encodable {
|
||||
func encode(to _: any Encoder) throws {}
|
||||
}
|
||||
|
||||
struct KeyedCodingPathEncodingProbe: Encodable {
|
||||
func encode(to encoder: any Encoder) throws {
|
||||
expectCodingPath(encoder.codingPath, strings: [], intValues: [])
|
||||
|
||||
var container = encoder.container(keyedBy: CodingPathProbeTopKey.self)
|
||||
expectCodingPath(container.codingPath, strings: [], intValues: [])
|
||||
|
||||
let superEncoder = container.superEncoder(forKey: .payload)
|
||||
expectCodingPath(superEncoder.codingPath, strings: ["payload"], intValues: [nil])
|
||||
|
||||
var metadata = container.nestedContainer(
|
||||
keyedBy: CodingPathProbeNestedKey.self,
|
||||
forKey: .metadata,
|
||||
)
|
||||
expectCodingPath(metadata.codingPath, strings: ["metadata"], intValues: [nil])
|
||||
try metadata.encode("Mihaela", forKey: .author)
|
||||
|
||||
var items = container.nestedUnkeyedContainer(forKey: .items)
|
||||
expectCodingPath(items.codingPath, strings: ["items"], intValues: [nil])
|
||||
|
||||
var item = items.nestedContainer(keyedBy: CodingPathProbeNestedKey.self)
|
||||
expectCodingPath(item.codingPath, strings: ["items", "0"], intValues: [nil, 0])
|
||||
try item.encode("Nested", forKey: .author)
|
||||
|
||||
try "child".encode(to: superEncoder)
|
||||
}
|
||||
}
|
||||
|
||||
struct KeyedCodingPathDecodingProbe: Decodable {
|
||||
init(from decoder: any Decoder) throws {
|
||||
expectCodingPath(decoder.codingPath, strings: [], intValues: [])
|
||||
|
||||
let container = try decoder.container(keyedBy: CodingPathProbeTopKey.self)
|
||||
expectCodingPath(container.codingPath, strings: [], intValues: [])
|
||||
|
||||
let superDecoder = try container.superDecoder(forKey: .payload)
|
||||
expectCodingPath(superDecoder.codingPath, strings: ["payload"], intValues: [nil])
|
||||
_ = try superDecoder.singleValueContainer().decode(String.self)
|
||||
|
||||
let metadata = try container.nestedContainer(
|
||||
keyedBy: CodingPathProbeNestedKey.self,
|
||||
forKey: .metadata,
|
||||
)
|
||||
expectCodingPath(metadata.codingPath, strings: ["metadata"], intValues: [nil])
|
||||
_ = try metadata.decode(String.self, forKey: .author)
|
||||
|
||||
var items = try container.nestedUnkeyedContainer(forKey: .items)
|
||||
expectCodingPath(items.codingPath, strings: ["items"], intValues: [nil])
|
||||
|
||||
let item = try items.nestedContainer(keyedBy: CodingPathProbeNestedKey.self)
|
||||
expectCodingPath(item.codingPath, strings: ["items", "0"], intValues: [nil, 0])
|
||||
_ = try item.decode(String.self, forKey: .author)
|
||||
}
|
||||
}
|
||||
|
||||
enum CodingPathProbeTopKey: String, CodingKey {
|
||||
case payload
|
||||
case metadata
|
||||
case items
|
||||
}
|
||||
|
||||
enum CodingPathProbeNestedKey: String, CodingKey {
|
||||
case author
|
||||
}
|
||||
|
||||
struct DynamicCodingKey: CodingKey {
|
||||
var stringValue: String
|
||||
var intValue: Int?
|
||||
|
||||
init(_ stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
intValue = nil
|
||||
}
|
||||
|
||||
init?(stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
intValue = nil
|
||||
}
|
||||
|
||||
init?(intValue: Int) {
|
||||
stringValue = "\(intValue)"
|
||||
self.intValue = intValue
|
||||
}
|
||||
}
|
||||
247
Tests/CodableCompatibilityTests.swift
Normal file
247
Tests/CodableCompatibilityTests.swift
Normal file
@ -0,0 +1,247 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
@Suite("Codable Compatibility")
|
||||
struct CodableCompatibilityTests {
|
||||
@Test("Dictionary-like dynamic keys decode from ordered mappings")
|
||||
func test_dictionaryLikeDynamicKeysDecodeFromOrderedMappings() throws {
|
||||
let decoded = try PureYAML.decode(
|
||||
DynamicSettings.self,
|
||||
from: dynamicSettingsValue,
|
||||
)
|
||||
|
||||
#expect(decoded == DynamicSettings(entries: [
|
||||
.init(key: "/users", value: 2),
|
||||
.init(key: "name.with.dot", value: 3),
|
||||
]))
|
||||
}
|
||||
|
||||
@Test("Dictionary-like dynamic keys encode exact values and YAML")
|
||||
func test_dictionaryLikeDynamicKeysEncodeExactValuesAndYAML() throws {
|
||||
let settings = DynamicSettings(entries: [
|
||||
.init(key: "/users", value: 2),
|
||||
.init(key: "name.with.dot", value: 3),
|
||||
])
|
||||
|
||||
let yaml = try PureYAML.encodeToYAML(settings)
|
||||
|
||||
#expect(try PureYAML.encode(settings) == dynamicSettingsValue)
|
||||
#expect(yaml == """
|
||||
/users: 2
|
||||
name.with.dot: 3
|
||||
|
||||
""")
|
||||
#expect(yaml.contains("/users: 2"))
|
||||
#expect(yaml.contains("name.with.dot: 3"))
|
||||
#expect(!yaml.contains("\"name.with.dot\""))
|
||||
#expect(!yaml.contains("null"))
|
||||
}
|
||||
|
||||
@Test("Dictionary values decode where YAML mapping semantics allow them")
|
||||
func test_dictionaryValuesDecodeWhereYAMLSemanticsAllowThem() throws {
|
||||
let decoded = try PureYAML.decode(
|
||||
[String: Int].self,
|
||||
from: .mapping(.init([
|
||||
.init(key: "count", value: .int(2)),
|
||||
])),
|
||||
)
|
||||
|
||||
#expect(decoded == ["count": 2])
|
||||
}
|
||||
|
||||
@Test("Nested keyed unkeyed and single-value interactions round trip exactly")
|
||||
func test_nestedKeyedUnkeyedAndSingleValueInteractionsRoundTripExactly() throws {
|
||||
let decoded = try PureYAML.decode(
|
||||
NestedCompatibilityDocument.self,
|
||||
from: nestedCompatibilityValue,
|
||||
)
|
||||
let yaml = try PureYAML.encodeToYAML(nestedCompatibilityFixture)
|
||||
|
||||
#expect(decoded == nestedCompatibilityFixture)
|
||||
#expect(try PureYAML.encode(nestedCompatibilityFixture) == nestedCompatibilityValue)
|
||||
#expect(yaml == """
|
||||
title: "Typed YAML"
|
||||
metadata:
|
||||
author: "Mihaela"
|
||||
rating: 5
|
||||
sections:
|
||||
-
|
||||
heading: "Intro"
|
||||
items:
|
||||
- "one"
|
||||
- "two"
|
||||
-
|
||||
heading: "Outro"
|
||||
items:
|
||||
|
||||
""")
|
||||
#expect(yaml.contains("sections:"))
|
||||
#expect(!yaml.contains("summary:"))
|
||||
}
|
||||
|
||||
@Test("Dynamic-key decoding reports exact punctuation paths")
|
||||
func test_dynamicKeyDecodingReportsExactPunctuationPaths() {
|
||||
expectDecodingError(
|
||||
.typeMismatch(
|
||||
expected: "Int",
|
||||
actual: "string",
|
||||
path: .init([.key("/users")]),
|
||||
),
|
||||
) {
|
||||
_ = try PureYAML.decode(DynamicSettings.self, from: .mapping(.init([
|
||||
.init(key: "/users", value: .string("two")),
|
||||
])))
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Dynamic-key decoding validates duplicate punctuation keys")
|
||||
func test_dynamicKeyDecodingValidatesDuplicatePunctuationKeys() {
|
||||
expectValidationError {
|
||||
_ = try PureYAML.decode(DynamicSettings.self, from: .mapping(.init([
|
||||
.init(key: "/users", value: .int(1)),
|
||||
.init(key: "/users", value: .int(2)),
|
||||
])))
|
||||
} check: { collection in
|
||||
#expect(collection.issues == [
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key '/users'",
|
||||
path: .init([.key("/users")]),
|
||||
),
|
||||
])
|
||||
#expect(collection.description == "error: Duplicate mapping key '/users' at $[\"/users\"]")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Keyed super decoders and encoders preserve exact payloads")
|
||||
func test_keyedSuperDecodersAndEncodersPreserveExactPayloads() throws {
|
||||
let value = PureYAML.Model.Value.mapping(.init([
|
||||
.init(key: "payload", value: .mapping(.init([
|
||||
.init(key: "value", value: .string("child")),
|
||||
]))),
|
||||
]))
|
||||
let box = KeyedSuperBox(payload: .init(value: "child"))
|
||||
|
||||
#expect(try PureYAML.decode(KeyedSuperBox.self, from: value) == box)
|
||||
#expect(try PureYAML.encode(box) == value)
|
||||
#expect(try PureYAML.encodeToYAML(box) == """
|
||||
payload:
|
||||
value: "child"
|
||||
|
||||
""")
|
||||
}
|
||||
|
||||
@Test("Keyed default super coders use the super key")
|
||||
func test_keyedDefaultSuperCodersUseTheSuperKey() throws {
|
||||
let value = PureYAML.Model.Value.mapping(.init([
|
||||
.init(key: "super", value: .mapping(.init([
|
||||
.init(key: "value", value: .string("child")),
|
||||
]))),
|
||||
]))
|
||||
let box = KeyedDefaultSuperBox(payload: .init(value: "child"))
|
||||
|
||||
#expect(try PureYAML.decode(KeyedDefaultSuperBox.self, from: value) == box)
|
||||
#expect(try PureYAML.encode(box) == value)
|
||||
#expect(try PureYAML.encodeToYAML(box) == """
|
||||
super:
|
||||
value: "child"
|
||||
|
||||
""")
|
||||
}
|
||||
|
||||
@Test("Keyed super encoders reserve distinct keys before values are written")
|
||||
func test_keyedSuperEncodersReserveDistinctKeysBeforeValuesAreWritten() throws {
|
||||
#expect(try PureYAML.encode(DelayedKeyedSuperEncoders()) == .mapping(.init([
|
||||
.init(key: "first", value: .string("one")),
|
||||
.init(key: "second", value: .string("two")),
|
||||
])))
|
||||
}
|
||||
|
||||
@Test("Nested coders expose exact coding paths")
|
||||
func test_nestedCodersExposeExactCodingPaths() throws {
|
||||
#expect(try PureYAML.encode(KeyedCodingPathEncodingProbe()) == codingPathProbeValue)
|
||||
|
||||
_ = try PureYAML.decode(
|
||||
KeyedCodingPathDecodingProbe.self,
|
||||
from: codingPathProbeValue,
|
||||
)
|
||||
}
|
||||
|
||||
@Test("Unsupported missing super decoder payload reports exact errors")
|
||||
func test_unsupportedMissingSuperDecoderPayloadReportsExactErrors() {
|
||||
expectDecodingError(
|
||||
.keyNotFound(key: "payload", path: .init([.key("payload")])),
|
||||
) {
|
||||
_ = try PureYAML.decode(MissingPayloadBox.self, from: .mapping(.init()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Unsupported missing default super decoder reports exact errors")
|
||||
func test_unsupportedMissingDefaultSuperDecoderReportsExactErrors() {
|
||||
expectDecodingError(
|
||||
.keyNotFound(key: "super", path: .init([.key("super")])),
|
||||
) {
|
||||
_ = try PureYAML.decode(MissingDefaultSuperBox.self, from: .mapping(.init()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Unsupported empty encodable reports exact errors")
|
||||
func test_unsupportedEmptyEncodableReportsExactErrors() {
|
||||
expectEncodingError(.noValueEncoded(path: .root)) {
|
||||
_ = try PureYAML.encode(NoValueEncoded())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func expectDecodingError(
|
||||
_ expected: PureYAML.Decoding.Error,
|
||||
operation: () throws -> some Any,
|
||||
) {
|
||||
do {
|
||||
_ = try operation()
|
||||
recordIssue("expected decoding error")
|
||||
} catch let error as PureYAML.Decoding.Error {
|
||||
#expect(error == expected)
|
||||
#expect(error.description == expected.description)
|
||||
} catch {
|
||||
recordIssue("unexpected error \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func expectEncodingError(
|
||||
_ expected: PureYAML.Encoding.Error,
|
||||
operation: () throws -> some Any,
|
||||
) {
|
||||
do {
|
||||
_ = try operation()
|
||||
recordIssue("expected encoding error")
|
||||
} catch let error as PureYAML.Encoding.Error {
|
||||
#expect(error == expected)
|
||||
#expect(error.description == expected.description)
|
||||
} catch {
|
||||
recordIssue("unexpected error \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func expectValidationError(
|
||||
operation: () throws -> some Any,
|
||||
check: (PureYAML.Validation.Issue.Collection) -> Void,
|
||||
) {
|
||||
do {
|
||||
_ = try operation()
|
||||
recordIssue("expected validation error")
|
||||
} catch let collection as PureYAML.Validation.Issue.Collection {
|
||||
check(collection)
|
||||
} catch {
|
||||
recordIssue("unexpected error \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func expectCodingPath(
|
||||
_ path: [any CodingKey],
|
||||
strings: [String],
|
||||
intValues: [Int?],
|
||||
) {
|
||||
#expect(path.map(\.stringValue) == strings)
|
||||
#expect(path.map(\.intValue) == intValues)
|
||||
}
|
||||
45
docs/rules/research-compatibility.md
Normal file
45
docs/rules/research-compatibility.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Research Compatibility Rules
|
||||
|
||||
PureYAML replaces behavior that is currently studied in the private
|
||||
`PureYAMLResearch` repository. Compatibility work must be research-led and
|
||||
implementation-independent.
|
||||
|
||||
## Mandatory Start Point
|
||||
|
||||
Before changing parser, scanner, composer, emitter, validation, or typed
|
||||
Codable conversion behavior:
|
||||
|
||||
- inspect the matching source, tests, or fixtures in `PureYAMLResearch`;
|
||||
- identify the observable behavior PureYAML should match or deliberately reject;
|
||||
- write PureYAML tests that pin that behavior before or with the implementation;
|
||||
- keep all public-package code original and dependency-free.
|
||||
|
||||
## Copying Boundary
|
||||
|
||||
Do not copy source from Yams, CYaml, libyaml, or research-only references into
|
||||
`Sources/`. Use the research repository for behavior discovery only.
|
||||
|
||||
Allowed:
|
||||
|
||||
- exact behavioral assertions in PureYAML tests;
|
||||
- attribution in `ATTRIBUTION.md` and design docs;
|
||||
- small independently written fixtures that describe input and expected output.
|
||||
|
||||
Forbidden:
|
||||
|
||||
- copied parser, scanner, emitter, or encoder implementation code;
|
||||
- C files, generated parser files, or external SwiftPM dependencies;
|
||||
- compatibility claims that were not checked against the research source or a
|
||||
deliberate PureYAML design decision.
|
||||
|
||||
## Test Contract
|
||||
|
||||
Every compatibility test must pin at least one of:
|
||||
|
||||
- exact parsed value tree;
|
||||
- exact emitted YAML;
|
||||
- exact decoded Swift value;
|
||||
- exact encoded value tree;
|
||||
- exact validation issue;
|
||||
- exact path-aware decoding or encoding error;
|
||||
- exact required absence check for fields that must not appear.
|
||||
Loading…
Reference in New Issue
Block a user