feat(coding): add unkeyed typed conversion
This commit is contained in:
parent
f80d7460ea
commit
5c3de786bf
@ -54,6 +54,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Add keyed typed Decodable and Encodable conversion for mapping-backed structs.
|
||||
- Validate YAML values before typed decoding so duplicate keys are rejected
|
||||
deterministically.
|
||||
- Add unkeyed typed Decodable and Encodable conversion for sequences, including
|
||||
nested sequences and sequence properties on keyed structs.
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
10
README.md
10
README.md
@ -101,8 +101,8 @@ let yaml = PureYAML.dump(document)
|
||||
try PureYAML.validate(document)
|
||||
```
|
||||
|
||||
Typed conversion is available for scalar values and keyed mapping-backed
|
||||
structs:
|
||||
Typed conversion is available for scalar values, keyed mapping-backed structs,
|
||||
and unkeyed sequences:
|
||||
|
||||
```swift
|
||||
struct Info: Codable {
|
||||
@ -115,12 +115,14 @@ let info = try PureYAML.decode(Info.self, from: """
|
||||
title: Example API
|
||||
version: 1
|
||||
""")
|
||||
let tags = try PureYAML.decode([String].self, from: """
|
||||
- Swift
|
||||
- YAML
|
||||
""")
|
||||
let value = try PureYAML.encode(42)
|
||||
let yaml = try PureYAML.encodeToYAML(info)
|
||||
```
|
||||
|
||||
Unkeyed sequences are still explicit future work; attempts to decode or encode
|
||||
arrays through typed conversion report exact path-aware errors.
|
||||
Typed decoding runs the default validator before constructing Swift values, so
|
||||
duplicate mapping keys are reported as validation issues instead of silently
|
||||
choosing one value.
|
||||
|
||||
19
Sources/Coding/IndexCodingKey.swift
Normal file
19
Sources/Coding/IndexCodingKey.swift
Normal file
@ -0,0 +1,19 @@
|
||||
struct IndexCodingKey: CodingKey {
|
||||
var stringValue: String
|
||||
var intValue: Int?
|
||||
|
||||
init(index: Int) {
|
||||
stringValue = "\(index)"
|
||||
intValue = index
|
||||
}
|
||||
|
||||
init?(stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
intValue = nil
|
||||
}
|
||||
|
||||
init?(intValue: Int) {
|
||||
stringValue = "\(intValue)"
|
||||
self.intValue = intValue
|
||||
}
|
||||
}
|
||||
@ -40,9 +40,19 @@ public extension PureYAML.Decoding {
|
||||
|
||||
public func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
|
||||
try validateIfNeeded()
|
||||
throw Error.unsupportedContainer(
|
||||
kind: "unkeyed",
|
||||
path: PureYAML.Validation.Path(codingPath: codingPath),
|
||||
guard case let .sequence(values) = value else {
|
||||
throw Error.typeMismatch(
|
||||
expected: "sequence",
|
||||
actual: value.kindDescription,
|
||||
path: PureYAML.Validation.Path(codingPath: codingPath),
|
||||
)
|
||||
}
|
||||
|
||||
return UnkeyedDecodingContainerImpl(
|
||||
values: values,
|
||||
codingPath: codingPath,
|
||||
userInfo: userInfo,
|
||||
validatesInput: validatesInput,
|
||||
)
|
||||
}
|
||||
|
||||
@ -52,6 +62,7 @@ public extension PureYAML.Decoding {
|
||||
value: value,
|
||||
codingPath: codingPath,
|
||||
userInfo: userInfo,
|
||||
validatesInput: validatesInput,
|
||||
)
|
||||
}
|
||||
|
||||
@ -89,6 +100,7 @@ private struct SingleValueContainer: SingleValueDecodingContainer {
|
||||
let value: PureYAML.Model.Value
|
||||
let codingPath: [any CodingKey]
|
||||
let userInfo: [CodingUserInfoKey: Any]
|
||||
let validatesInput: Bool
|
||||
|
||||
func decodeNil() -> Bool {
|
||||
value == .null
|
||||
@ -171,6 +183,7 @@ private struct SingleValueContainer: SingleValueDecodingContainer {
|
||||
value: value,
|
||||
codingPath: codingPath,
|
||||
userInfo: userInfo,
|
||||
validatesInput: validatesInput,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@ -14,10 +14,7 @@ public extension PureYAML.Decoding {
|
||||
key: String,
|
||||
path: PureYAML.Validation.Path,
|
||||
)
|
||||
case unsupportedContainer(
|
||||
kind: String,
|
||||
path: PureYAML.Validation.Path,
|
||||
)
|
||||
case valueNotFound(path: PureYAML.Validation.Path)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
@ -27,8 +24,8 @@ public extension PureYAML.Decoding {
|
||||
"\(type) value is outside PureYAML integer range at \(path)"
|
||||
case let .keyNotFound(key, path):
|
||||
"Missing required key '\(key)' at \(path)"
|
||||
case let .unsupportedContainer(kind, path):
|
||||
"Unsupported \(kind) decoding container at \(path)"
|
||||
case let .valueNotFound(path):
|
||||
"No YAML value found at \(path)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,9 +100,19 @@ struct KeyedDecodingContainerImpl<Key: CodingKey>: KeyedDecodingContainerProtoco
|
||||
func nestedUnkeyedContainer(forKey key: Key) throws -> any UnkeyedDecodingContainer {
|
||||
let value = try value(for: key)
|
||||
try validateIfNeeded(value, forKey: key)
|
||||
throw PureYAML.Decoding.Error.unsupportedContainer(
|
||||
kind: "unkeyed",
|
||||
path: PureYAML.Validation.Path(codingPath: codingPath + [key]),
|
||||
guard case let .sequence(values) = value else {
|
||||
throw PureYAML.Decoding.Error.typeMismatch(
|
||||
expected: "sequence",
|
||||
actual: value.kindDescription,
|
||||
path: PureYAML.Validation.Path(codingPath: codingPath + [key]),
|
||||
)
|
||||
}
|
||||
|
||||
return UnkeyedDecodingContainerImpl(
|
||||
values: values,
|
||||
codingPath: codingPath + [key],
|
||||
userInfo: userInfo,
|
||||
validatesInput: validatesInput,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
160
Sources/Decoding/Decoding.UnkeyedContainer.swift
Normal file
160
Sources/Decoding/Decoding.UnkeyedContainer.swift
Normal file
@ -0,0 +1,160 @@
|
||||
struct UnkeyedDecodingContainerImpl: UnkeyedDecodingContainer {
|
||||
let values: [PureYAML.Model.Value]
|
||||
let codingPath: [any CodingKey]
|
||||
let userInfo: [CodingUserInfoKey: Any]
|
||||
let validatesInput: Bool
|
||||
var currentIndex: Int = 0
|
||||
|
||||
var count: Int? {
|
||||
values.count
|
||||
}
|
||||
|
||||
var isAtEnd: Bool {
|
||||
currentIndex >= values.count
|
||||
}
|
||||
|
||||
mutating func decodeNil() throws -> Bool {
|
||||
let value = try currentValue()
|
||||
guard value == .null else {
|
||||
return false
|
||||
}
|
||||
currentIndex += 1
|
||||
return true
|
||||
}
|
||||
|
||||
mutating func decode(_: Bool.Type) throws -> Bool {
|
||||
try decodeSingleValue(Bool.self)
|
||||
}
|
||||
|
||||
mutating func decode(_: String.Type) throws -> String {
|
||||
try decodeSingleValue(String.self)
|
||||
}
|
||||
|
||||
mutating func decode(_: Double.Type) throws -> Double {
|
||||
try decodeSingleValue(Double.self)
|
||||
}
|
||||
|
||||
mutating func decode(_: Float.Type) throws -> Float {
|
||||
try decodeSingleValue(Float.self)
|
||||
}
|
||||
|
||||
mutating func decode(_: Int.Type) throws -> Int {
|
||||
try decodeSingleValue(Int.self)
|
||||
}
|
||||
|
||||
mutating func decode(_: Int8.Type) throws -> Int8 {
|
||||
try decodeSingleValue(Int8.self)
|
||||
}
|
||||
|
||||
mutating func decode(_: Int16.Type) throws -> Int16 {
|
||||
try decodeSingleValue(Int16.self)
|
||||
}
|
||||
|
||||
mutating func decode(_: Int32.Type) throws -> Int32 {
|
||||
try decodeSingleValue(Int32.self)
|
||||
}
|
||||
|
||||
mutating func decode(_: Int64.Type) throws -> Int64 {
|
||||
try decodeSingleValue(Int64.self)
|
||||
}
|
||||
|
||||
mutating func decode(_: UInt.Type) throws -> UInt {
|
||||
try decodeSingleValue(UInt.self)
|
||||
}
|
||||
|
||||
mutating func decode(_: UInt8.Type) throws -> UInt8 {
|
||||
try decodeSingleValue(UInt8.self)
|
||||
}
|
||||
|
||||
mutating func decode(_: UInt16.Type) throws -> UInt16 {
|
||||
try decodeSingleValue(UInt16.self)
|
||||
}
|
||||
|
||||
mutating func decode(_: UInt32.Type) throws -> UInt32 {
|
||||
try decodeSingleValue(UInt32.self)
|
||||
}
|
||||
|
||||
mutating func decode(_: UInt64.Type) throws -> UInt64 {
|
||||
try decodeSingleValue(UInt64.self)
|
||||
}
|
||||
|
||||
mutating func decode<Value: Decodable>(_: Value.Type) throws -> Value {
|
||||
let value = try Value(from: decoderForCurrentIndex())
|
||||
currentIndex += 1
|
||||
return value
|
||||
}
|
||||
|
||||
mutating func nestedContainer<NestedKey: CodingKey>(
|
||||
keyedBy _: NestedKey.Type,
|
||||
) throws -> KeyedDecodingContainer<NestedKey> {
|
||||
let index = currentIndex
|
||||
let value = try currentValue()
|
||||
guard case let .mapping(mapping) = value else {
|
||||
throw PureYAML.Decoding.Error.typeMismatch(
|
||||
expected: "mapping",
|
||||
actual: value.kindDescription,
|
||||
path: PureYAML.Validation.Path(codingPath: codingPath + [IndexCodingKey(index: index)]),
|
||||
)
|
||||
}
|
||||
|
||||
currentIndex += 1
|
||||
return KeyedDecodingContainer(KeyedDecodingContainerImpl<NestedKey>(
|
||||
mapping: mapping,
|
||||
codingPath: codingPath + [IndexCodingKey(index: index)],
|
||||
userInfo: userInfo,
|
||||
validatesInput: validatesInput,
|
||||
))
|
||||
}
|
||||
|
||||
mutating func nestedUnkeyedContainer() throws -> any UnkeyedDecodingContainer {
|
||||
let index = currentIndex
|
||||
let value = try currentValue()
|
||||
guard case let .sequence(values) = value else {
|
||||
throw PureYAML.Decoding.Error.typeMismatch(
|
||||
expected: "sequence",
|
||||
actual: value.kindDescription,
|
||||
path: PureYAML.Validation.Path(codingPath: codingPath + [IndexCodingKey(index: index)]),
|
||||
)
|
||||
}
|
||||
|
||||
currentIndex += 1
|
||||
return UnkeyedDecodingContainerImpl(
|
||||
values: values,
|
||||
codingPath: codingPath + [IndexCodingKey(index: index)],
|
||||
userInfo: userInfo,
|
||||
validatesInput: validatesInput,
|
||||
)
|
||||
}
|
||||
|
||||
mutating func superDecoder() throws -> any Swift.Decoder {
|
||||
let decoder = try decoderForCurrentIndex()
|
||||
currentIndex += 1
|
||||
return decoder
|
||||
}
|
||||
|
||||
private mutating func decodeSingleValue<Value: Decodable>(
|
||||
_ type: Value.Type,
|
||||
) throws -> Value {
|
||||
let value = try decoderForCurrentIndex().singleValueContainer().decode(type)
|
||||
currentIndex += 1
|
||||
return value
|
||||
}
|
||||
|
||||
private func decoderForCurrentIndex() throws -> PureYAML.Decoding.Decoder {
|
||||
try PureYAML.Decoding.Decoder(
|
||||
value: currentValue(),
|
||||
codingPath: codingPath + [IndexCodingKey(index: currentIndex)],
|
||||
userInfo: userInfo,
|
||||
validatesInput: validatesInput,
|
||||
)
|
||||
}
|
||||
|
||||
private func currentValue() throws -> PureYAML.Model.Value {
|
||||
guard currentIndex < values.count else {
|
||||
throw PureYAML.Decoding.Error.valueNotFound(
|
||||
path: PureYAML.Validation.Path(codingPath: codingPath + [IndexCodingKey(index: currentIndex)]),
|
||||
)
|
||||
}
|
||||
return values[currentIndex]
|
||||
}
|
||||
}
|
||||
@ -50,11 +50,9 @@ public extension PureYAML.Encoding {
|
||||
}
|
||||
|
||||
public func unkeyedContainer() -> any UnkeyedEncodingContainer {
|
||||
storage.fail(.unsupportedContainer(
|
||||
kind: "unkeyed",
|
||||
path: PureYAML.Validation.Path(codingPath: codingPath),
|
||||
))
|
||||
return UnsupportedUnkeyedEncodingContainer(
|
||||
storage.set(.sequence([]))
|
||||
return UnkeyedEncodingContainerImpl(
|
||||
storage: storage,
|
||||
codingPath: codingPath,
|
||||
userInfo: userInfo,
|
||||
)
|
||||
@ -76,19 +74,24 @@ extension PureYAML.Encoding.Encoder {
|
||||
var error: PureYAML.Encoding.Error?
|
||||
private let parent: Storage?
|
||||
private let parentKey: String?
|
||||
private let parentIndex: Int?
|
||||
|
||||
init(
|
||||
parent: Storage? = nil,
|
||||
parentKey: String? = nil,
|
||||
parentIndex: Int? = nil,
|
||||
) {
|
||||
self.parent = parent
|
||||
self.parentKey = parentKey
|
||||
self.parentIndex = parentIndex
|
||||
}
|
||||
|
||||
func set(_ value: PureYAML.Model.Value) {
|
||||
self.value = value
|
||||
if let parent, let parentKey {
|
||||
parent.set(value, forKey: parentKey)
|
||||
} else if let parent, let parentIndex {
|
||||
parent.set(value, atIndex: parentIndex)
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,6 +113,35 @@ extension PureYAML.Encoding.Encoder {
|
||||
set(.mapping(mapping))
|
||||
}
|
||||
|
||||
func append(_ value: PureYAML.Model.Value) {
|
||||
var values: [PureYAML.Model.Value] = if case let .sequence(existing) = self.value {
|
||||
existing
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
values.append(value)
|
||||
set(.sequence(values))
|
||||
}
|
||||
|
||||
func set(
|
||||
_ value: PureYAML.Model.Value,
|
||||
atIndex index: Int,
|
||||
) {
|
||||
var values: [PureYAML.Model.Value] = if case let .sequence(existing) = self.value {
|
||||
existing
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
if index == values.count {
|
||||
values.append(value)
|
||||
} else if index < values.count {
|
||||
values[index] = value
|
||||
}
|
||||
set(.sequence(values))
|
||||
}
|
||||
|
||||
func fail(_ error: PureYAML.Encoding.Error) {
|
||||
self.error = error
|
||||
parent?.fail(error)
|
||||
@ -209,104 +241,3 @@ private struct SingleValueContainer: SingleValueEncodingContainer {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct UnsupportedKeyedEncodingContainer<Key: CodingKey>: KeyedEncodingContainerProtocol {
|
||||
var codingPath: [any CodingKey]
|
||||
var userInfo: [CodingUserInfoKey: Any] = [:]
|
||||
|
||||
mutating func encodeNil(forKey key: Key) throws {
|
||||
throw unsupported(key)
|
||||
}
|
||||
|
||||
mutating func encode(_: Bool, forKey key: Key) throws {
|
||||
throw unsupported(key)
|
||||
}
|
||||
|
||||
mutating func encode(_: String, forKey key: Key) throws {
|
||||
throw unsupported(key)
|
||||
}
|
||||
|
||||
mutating func encode(_: Double, forKey key: Key) throws {
|
||||
throw unsupported(key)
|
||||
}
|
||||
|
||||
mutating func encode(_: Float, forKey key: Key) throws {
|
||||
throw unsupported(key)
|
||||
}
|
||||
|
||||
mutating func encode(_: Int, forKey key: Key) throws {
|
||||
throw unsupported(key)
|
||||
}
|
||||
|
||||
mutating func encode(_: Int8, forKey key: Key) throws {
|
||||
throw unsupported(key)
|
||||
}
|
||||
|
||||
mutating func encode(_: Int16, forKey key: Key) throws {
|
||||
throw unsupported(key)
|
||||
}
|
||||
|
||||
mutating func encode(_: Int32, forKey key: Key) throws {
|
||||
throw unsupported(key)
|
||||
}
|
||||
|
||||
mutating func encode(_: Int64, forKey key: Key) throws {
|
||||
throw unsupported(key)
|
||||
}
|
||||
|
||||
mutating func encode(_: UInt, forKey key: Key) throws {
|
||||
throw unsupported(key)
|
||||
}
|
||||
|
||||
mutating func encode(_: UInt8, forKey key: Key) throws {
|
||||
throw unsupported(key)
|
||||
}
|
||||
|
||||
mutating func encode(_: UInt16, forKey key: Key) throws {
|
||||
throw unsupported(key)
|
||||
}
|
||||
|
||||
mutating func encode(_: UInt32, forKey key: Key) throws {
|
||||
throw unsupported(key)
|
||||
}
|
||||
|
||||
mutating func encode(_: UInt64, forKey key: Key) throws {
|
||||
throw unsupported(key)
|
||||
}
|
||||
|
||||
mutating func encode(_: some Encodable, forKey key: Key) throws {
|
||||
throw unsupported(key)
|
||||
}
|
||||
|
||||
mutating func nestedContainer<NestedKey: CodingKey>(
|
||||
keyedBy _: NestedKey.Type,
|
||||
forKey key: Key,
|
||||
) -> KeyedEncodingContainer<NestedKey> {
|
||||
KeyedEncodingContainer(UnsupportedKeyedEncodingContainer<NestedKey>(
|
||||
codingPath: codingPath + [key],
|
||||
userInfo: userInfo,
|
||||
))
|
||||
}
|
||||
|
||||
mutating func nestedUnkeyedContainer(forKey key: Key) -> any UnkeyedEncodingContainer {
|
||||
UnsupportedUnkeyedEncodingContainer(
|
||||
codingPath: codingPath + [key],
|
||||
userInfo: userInfo,
|
||||
)
|
||||
}
|
||||
|
||||
mutating func superEncoder() -> any Swift.Encoder {
|
||||
PureYAML.Encoding.Encoder(codingPath: codingPath, userInfo: userInfo)
|
||||
}
|
||||
|
||||
mutating func superEncoder(forKey key: Key) -> any Swift.Encoder {
|
||||
PureYAML.Encoding.Encoder(codingPath: codingPath + [key], userInfo: userInfo)
|
||||
}
|
||||
|
||||
private func unsupported(_ key: Key) -> PureYAML.Encoding.Error {
|
||||
.unsupportedContainer(
|
||||
kind: "keyed",
|
||||
path: PureYAML.Validation.Path(codingPath: codingPath + [key]),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,6 @@
|
||||
public extension PureYAML.Encoding {
|
||||
/// Error reported by typed YAML encoding.
|
||||
enum Error: Swift.Error, Equatable, Sendable, CustomStringConvertible {
|
||||
case unsupportedContainer(
|
||||
kind: String,
|
||||
path: PureYAML.Validation.Path,
|
||||
)
|
||||
case integerOutOfRange(
|
||||
type: String,
|
||||
path: PureYAML.Validation.Path,
|
||||
@ -13,8 +9,6 @@ public extension PureYAML.Encoding {
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case let .unsupportedContainer(kind, path):
|
||||
"Unsupported \(kind) encoding container at \(path)"
|
||||
case let .integerOutOfRange(type, path):
|
||||
"\(type) value is outside PureYAML integer range at \(path)"
|
||||
case let .noValueEncoded(path):
|
||||
|
||||
@ -122,11 +122,8 @@ struct KeyedEncodingContainerImpl<Key: CodingKey>: KeyedEncodingContainerProtoco
|
||||
parent: storage,
|
||||
parentKey: key.stringValue,
|
||||
)
|
||||
childStorage.fail(.unsupportedContainer(
|
||||
kind: "unkeyed",
|
||||
path: PureYAML.Validation.Path(codingPath: codingPath + [key]),
|
||||
))
|
||||
return UnsupportedUnkeyedEncodingContainer(
|
||||
return UnkeyedEncodingContainerImpl(
|
||||
storage: childStorage,
|
||||
codingPath: codingPath + [key],
|
||||
userInfo: userInfo,
|
||||
)
|
||||
|
||||
160
Sources/Encoding/Encoding.UnkeyedContainer.swift
Normal file
160
Sources/Encoding/Encoding.UnkeyedContainer.swift
Normal file
@ -0,0 +1,160 @@
|
||||
struct UnkeyedEncodingContainerImpl: UnkeyedEncodingContainer {
|
||||
let storage: PureYAML.Encoding.Encoder.Storage
|
||||
var codingPath: [any CodingKey]
|
||||
var userInfo: [CodingUserInfoKey: Any]
|
||||
var count: Int {
|
||||
if case let .sequence(values) = storage.value {
|
||||
return values.count
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
init(
|
||||
storage: PureYAML.Encoding.Encoder.Storage,
|
||||
codingPath: [any CodingKey],
|
||||
userInfo: [CodingUserInfoKey: Any],
|
||||
) {
|
||||
self.storage = storage
|
||||
self.codingPath = codingPath
|
||||
self.userInfo = userInfo
|
||||
if storage.value == nil {
|
||||
storage.set(.sequence([]))
|
||||
}
|
||||
}
|
||||
|
||||
mutating func encodeNil() throws {
|
||||
storage.append(.null)
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Bool) throws {
|
||||
storage.append(.bool(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: String) throws {
|
||||
storage.append(.string(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Double) throws {
|
||||
storage.append(.double(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Float) throws {
|
||||
storage.append(.double(Double(value)))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int) throws {
|
||||
storage.append(.int(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int8) throws {
|
||||
storage.append(.int(Int(value)))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int16) throws {
|
||||
storage.append(.int(Int(value)))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int32) throws {
|
||||
storage.append(.int(Int(value)))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: Int64) throws {
|
||||
guard let value = Int(exactly: value) else {
|
||||
throw integerOutOfRange("Int64")
|
||||
}
|
||||
storage.append(.int(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt) throws {
|
||||
guard let value = Int(exactly: value) else {
|
||||
throw integerOutOfRange("UInt")
|
||||
}
|
||||
storage.append(.int(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt8) throws {
|
||||
storage.append(.int(Int(value)))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt16) throws {
|
||||
storage.append(.int(Int(value)))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt32) throws {
|
||||
guard let value = Int(exactly: value) else {
|
||||
throw integerOutOfRange("UInt32")
|
||||
}
|
||||
storage.append(.int(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: UInt64) throws {
|
||||
guard let value = Int(exactly: value) else {
|
||||
throw integerOutOfRange("UInt64")
|
||||
}
|
||||
storage.append(.int(value))
|
||||
}
|
||||
|
||||
mutating func encode(_ value: some Encodable) throws {
|
||||
let index = count
|
||||
let childStorage = PureYAML.Encoding.Encoder.Storage(
|
||||
parent: storage,
|
||||
parentIndex: index,
|
||||
)
|
||||
let childEncoder = PureYAML.Encoding.Encoder(
|
||||
codingPath: codingPath + [IndexCodingKey(index: index)],
|
||||
userInfo: userInfo,
|
||||
storage: childStorage,
|
||||
)
|
||||
try value.encode(to: childEncoder)
|
||||
_ = try childEncoder.encodedValue()
|
||||
}
|
||||
|
||||
mutating func nestedContainer<NestedKey: CodingKey>(
|
||||
keyedBy _: NestedKey.Type,
|
||||
) -> KeyedEncodingContainer<NestedKey> {
|
||||
let index = count
|
||||
let childStorage = PureYAML.Encoding.Encoder.Storage(
|
||||
parent: storage,
|
||||
parentIndex: index,
|
||||
)
|
||||
return KeyedEncodingContainer(KeyedEncodingContainerImpl<NestedKey>(
|
||||
storage: childStorage,
|
||||
codingPath: codingPath + [IndexCodingKey(index: index)],
|
||||
userInfo: userInfo,
|
||||
))
|
||||
}
|
||||
|
||||
mutating func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer {
|
||||
let index = count
|
||||
let childStorage = PureYAML.Encoding.Encoder.Storage(
|
||||
parent: storage,
|
||||
parentIndex: index,
|
||||
)
|
||||
return UnkeyedEncodingContainerImpl(
|
||||
storage: childStorage,
|
||||
codingPath: codingPath + [IndexCodingKey(index: index)],
|
||||
userInfo: userInfo,
|
||||
)
|
||||
}
|
||||
|
||||
mutating func superEncoder() -> any Swift.Encoder {
|
||||
let index = count
|
||||
storage.append(.null)
|
||||
let childStorage = PureYAML.Encoding.Encoder.Storage(
|
||||
parent: storage,
|
||||
parentIndex: index,
|
||||
)
|
||||
return PureYAML.Encoding.Encoder(
|
||||
codingPath: codingPath + [IndexCodingKey(index: index)],
|
||||
userInfo: userInfo,
|
||||
storage: childStorage,
|
||||
)
|
||||
}
|
||||
|
||||
private func integerOutOfRange(_ type: String) -> PureYAML.Encoding.Error {
|
||||
.integerOutOfRange(
|
||||
type: type,
|
||||
path: PureYAML.Validation.Path(codingPath: codingPath + [IndexCodingKey(index: count)]),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,123 +0,0 @@
|
||||
struct UnsupportedUnkeyedEncodingContainer: UnkeyedEncodingContainer {
|
||||
var codingPath: [any CodingKey]
|
||||
var userInfo: [CodingUserInfoKey: Any]
|
||||
var count: Int {
|
||||
0
|
||||
}
|
||||
|
||||
mutating func encodeNil() throws {
|
||||
throw unsupported()
|
||||
}
|
||||
|
||||
mutating func encode(_: Bool) throws {
|
||||
throw unsupported()
|
||||
}
|
||||
|
||||
mutating func encode(_: String) throws {
|
||||
throw unsupported()
|
||||
}
|
||||
|
||||
mutating func encode(_: Double) throws {
|
||||
throw unsupported()
|
||||
}
|
||||
|
||||
mutating func encode(_: Float) throws {
|
||||
throw unsupported()
|
||||
}
|
||||
|
||||
mutating func encode(_: Int) throws {
|
||||
throw unsupported()
|
||||
}
|
||||
|
||||
mutating func encode(_: Int8) throws {
|
||||
throw unsupported()
|
||||
}
|
||||
|
||||
mutating func encode(_: Int16) throws {
|
||||
throw unsupported()
|
||||
}
|
||||
|
||||
mutating func encode(_: Int32) throws {
|
||||
throw unsupported()
|
||||
}
|
||||
|
||||
mutating func encode(_: Int64) throws {
|
||||
throw unsupported()
|
||||
}
|
||||
|
||||
mutating func encode(_: UInt) throws {
|
||||
throw unsupported()
|
||||
}
|
||||
|
||||
mutating func encode(_: UInt8) throws {
|
||||
throw unsupported()
|
||||
}
|
||||
|
||||
mutating func encode(_: UInt16) throws {
|
||||
throw unsupported()
|
||||
}
|
||||
|
||||
mutating func encode(_: UInt32) throws {
|
||||
throw unsupported()
|
||||
}
|
||||
|
||||
mutating func encode(_: UInt64) throws {
|
||||
throw unsupported()
|
||||
}
|
||||
|
||||
mutating func encode(_: some Encodable) throws {
|
||||
throw unsupported()
|
||||
}
|
||||
|
||||
mutating func nestedContainer<NestedKey: CodingKey>(
|
||||
keyedBy _: NestedKey.Type,
|
||||
) -> KeyedEncodingContainer<NestedKey> {
|
||||
KeyedEncodingContainer(UnsupportedKeyedEncodingContainer<NestedKey>(
|
||||
codingPath: codingPath + [IndexCodingKey(index: count)],
|
||||
userInfo: userInfo,
|
||||
))
|
||||
}
|
||||
|
||||
mutating func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer {
|
||||
UnsupportedUnkeyedEncodingContainer(
|
||||
codingPath: codingPath + [IndexCodingKey(index: count)],
|
||||
userInfo: userInfo,
|
||||
)
|
||||
}
|
||||
|
||||
mutating func superEncoder() -> any Swift.Encoder {
|
||||
PureYAML.Encoding.Encoder(
|
||||
codingPath: codingPath + [IndexCodingKey(index: count)],
|
||||
userInfo: userInfo,
|
||||
)
|
||||
}
|
||||
|
||||
private func unsupported() -> PureYAML.Encoding.Error {
|
||||
.unsupportedContainer(
|
||||
kind: "unkeyed",
|
||||
path: PureYAML.Validation.Path(
|
||||
codingPath: codingPath + [IndexCodingKey(index: count)],
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private struct IndexCodingKey: CodingKey {
|
||||
var stringValue: String
|
||||
var intValue: Int?
|
||||
|
||||
init(index: Int) {
|
||||
stringValue = "\(index)"
|
||||
intValue = index
|
||||
}
|
||||
|
||||
init?(stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
intValue = nil
|
||||
}
|
||||
|
||||
init?(intValue: Int) {
|
||||
stringValue = "\(intValue)"
|
||||
self.intValue = intValue
|
||||
}
|
||||
}
|
||||
@ -68,30 +68,6 @@ struct CodingTests {
|
||||
testCase.expectFailure()
|
||||
}
|
||||
|
||||
@Test("Unsupported unkeyed decoding container reports exact errors")
|
||||
func test_unsupportedUnkeyedDecodingContainerReportsExactErrors() {
|
||||
expectDecodingError(
|
||||
.unsupportedContainer(kind: "unkeyed", path: .root),
|
||||
) {
|
||||
_ = try PureYAML.decode([Int].self, from: .sequence([.int(1)]))
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Unsupported unkeyed encoding containers report exact errors")
|
||||
func test_unsupportedUnkeyedEncodingContainersReportExactErrors() {
|
||||
expectEncodingError(
|
||||
.unsupportedContainer(kind: "unkeyed", path: .init([.index(0)])),
|
||||
) {
|
||||
_ = try PureYAML.encode([1, 2])
|
||||
}
|
||||
|
||||
expectEncodingError(
|
||||
.unsupportedContainer(kind: "unkeyed", path: .root),
|
||||
) {
|
||||
_ = try PureYAML.encode([Int]())
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Out of range integer encoding reports exact errors")
|
||||
func test_outOfRangeIntegerEncodingReportsExactErrors() {
|
||||
expectEncodingError(
|
||||
@ -109,11 +85,6 @@ struct CodingTests {
|
||||
path: .root,
|
||||
).description == "Expected String at $, found int")
|
||||
|
||||
#expect(PureYAML.Encoding.Error.unsupportedContainer(
|
||||
kind: "keyed",
|
||||
path: .init([.key("name")]),
|
||||
).description == "Unsupported keyed encoding container at $.name")
|
||||
|
||||
#expect(PureYAML.Decoding.Error.integerOutOfRange(
|
||||
type: "Int8",
|
||||
path: .root,
|
||||
|
||||
@ -132,23 +132,11 @@ struct KeyedCodingTests {
|
||||
|
||||
@Test("Keyed encoding failures report exact errors")
|
||||
func test_keyedEncodingFailuresReportExactErrors() {
|
||||
expectEncodingError(
|
||||
.unsupportedContainer(kind: "unkeyed", path: .init([.key("items"), .index(0)])),
|
||||
) {
|
||||
_ = try PureYAML.encode(UnsupportedList(items: [1]))
|
||||
}
|
||||
|
||||
expectEncodingError(
|
||||
.integerOutOfRange(type: "UInt64", path: .init([.key("value")])),
|
||||
) {
|
||||
_ = try PureYAML.encode(WideInteger(value: UInt64.max))
|
||||
}
|
||||
|
||||
expectEncodingError(
|
||||
.unsupportedContainer(kind: "unkeyed", path: .init([.key("items")])),
|
||||
) {
|
||||
_ = try PureYAML.encode(UnsupportedListThenTitle())
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Keyed coding error descriptions are exact")
|
||||
@ -164,10 +152,9 @@ struct KeyedCodingTests {
|
||||
path: .root,
|
||||
).description == "Expected mapping at $, found string")
|
||||
|
||||
#expect(PureYAML.Encoding.Error.unsupportedContainer(
|
||||
kind: "unkeyed",
|
||||
path: .init([.key("items")]),
|
||||
).description == "Unsupported unkeyed encoding container at $.items")
|
||||
#expect(PureYAML.Decoding.Error.valueNotFound(
|
||||
path: .init([.index(1)]),
|
||||
).description == "No YAML value found at $[1]")
|
||||
}
|
||||
}
|
||||
|
||||
@ -222,14 +209,6 @@ private let keyedDecodingErrorCases: [KeyedDecodingErrorCase] = [
|
||||
expected: .typeMismatch(expected: "mapping", actual: "string", path: .root),
|
||||
kind: .titleOnly,
|
||||
),
|
||||
.init(
|
||||
name: "unkeyed property remains unsupported",
|
||||
value: .mapping(.init([
|
||||
.init(key: "items", value: .sequence([.int(1)])),
|
||||
])),
|
||||
expected: .unsupportedContainer(kind: "unkeyed", path: .init([.key("items")])),
|
||||
kind: .list,
|
||||
),
|
||||
]
|
||||
|
||||
private struct KeyedArticle: Codable, Equatable {
|
||||
@ -259,27 +238,10 @@ private struct TitleOnly: Codable, Equatable {
|
||||
var title: String
|
||||
}
|
||||
|
||||
private struct UnsupportedList: Codable, Equatable {
|
||||
var items: [Int]
|
||||
}
|
||||
|
||||
private struct WideInteger: Encodable {
|
||||
var value: UInt64
|
||||
}
|
||||
|
||||
private struct UnsupportedListThenTitle: Encodable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case items
|
||||
case title
|
||||
}
|
||||
|
||||
func encode(to encoder: any Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
_ = container.nestedUnkeyedContainer(forKey: .items)
|
||||
try container.encode("after", forKey: .title)
|
||||
}
|
||||
}
|
||||
|
||||
private struct TestCodingKey: CodingKey {
|
||||
var stringValue: String
|
||||
var intValue: Int?
|
||||
@ -304,7 +266,6 @@ struct KeyedDecodingErrorCase: CustomStringConvertible {
|
||||
enum Kind {
|
||||
case article
|
||||
case titleOnly
|
||||
case list
|
||||
}
|
||||
|
||||
var name: String
|
||||
@ -323,8 +284,6 @@ struct KeyedDecodingErrorCase: CustomStringConvertible {
|
||||
_ = try PureYAML.decode(KeyedArticle.self, from: value)
|
||||
case .titleOnly:
|
||||
_ = try PureYAML.decode(TitleOnly.self, from: value)
|
||||
case .list:
|
||||
_ = try PureYAML.decode(UnsupportedList.self, from: value)
|
||||
}
|
||||
recordIssue("expected decoding error")
|
||||
} catch let error as PureYAML.Decoding.Error {
|
||||
|
||||
301
Tests/UnkeyedCodingTests.swift
Normal file
301
Tests/UnkeyedCodingTests.swift
Normal file
@ -0,0 +1,301 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
@Suite("Unkeyed Coding")
|
||||
struct UnkeyedCodingTests {
|
||||
@Test("Decodes unkeyed sequences with scalars optionals nesting and keyed elements")
|
||||
func test_decodesUnkeyedSequencesWithScalarsOptionalsNestingAndKeyedElements() throws {
|
||||
#expect(try PureYAML.decode([Int].self, from: .sequence([.int(1), .int(2)])) == [1, 2])
|
||||
|
||||
let optional = try PureYAML.decode([String?].self, from: .sequence([
|
||||
.string("one"),
|
||||
.null,
|
||||
.string("three"),
|
||||
]))
|
||||
#expect(optional == ["one", nil, "three"])
|
||||
|
||||
#expect(try PureYAML.decode([[Int]].self, from: nestedIntegerValue) == [[1, 2], []])
|
||||
#expect(try PureYAML.decode([ListedArticle].self, from: .sequence([
|
||||
listedArticleValue,
|
||||
])) == [listedArticle])
|
||||
#expect(try PureYAML.decode(ListedArticle.self, from: listedArticleValue) == listedArticle)
|
||||
}
|
||||
|
||||
@Test("Parses YAML sequences before unkeyed decoding")
|
||||
func test_parsesYAMLSequencesBeforeUnkeyedDecoding() throws {
|
||||
let decoded = try PureYAML.decode([String].self, from: """
|
||||
- one
|
||||
- two
|
||||
""")
|
||||
|
||||
#expect(decoded == ["one", "two"])
|
||||
}
|
||||
|
||||
@Test("Encodes unkeyed sequences as exact value trees and YAML")
|
||||
func test_encodesUnkeyedSequencesAsExactValueTreesAndYAML() throws {
|
||||
let optional: [String?] = ["one", nil, "three"]
|
||||
|
||||
#expect(try PureYAML.encode([1, 2]) == .sequence([.int(1), .int(2)]))
|
||||
#expect(try PureYAML.encode(optional) == .sequence([
|
||||
.string("one"),
|
||||
.null,
|
||||
.string("three"),
|
||||
]))
|
||||
#expect(try PureYAML.encode([[1, 2], []]) == nestedIntegerValue)
|
||||
#expect(try PureYAML.encode([listedArticle]) == .sequence([listedArticleValue]))
|
||||
#expect(try PureYAML.encode(listedArticle) == listedArticleValue)
|
||||
#expect(try PureYAML.encode([Int]()) == .sequence([]))
|
||||
#expect(try PureYAML.encodeToYAML([1, 2]) == """
|
||||
- 1
|
||||
- 2
|
||||
|
||||
""")
|
||||
#expect(try PureYAML.encodeToYAML([Int]()) == "\n")
|
||||
}
|
||||
|
||||
@Test("Unkeyed super encoders reserve distinct indexes before values are written")
|
||||
func test_unkeyedSuperEncodersReserveDistinctIndexesBeforeValuesAreWritten() throws {
|
||||
#expect(try PureYAML.encode(SuperEncodedPair()) == .sequence([
|
||||
.string("one"),
|
||||
.string("two"),
|
||||
]))
|
||||
}
|
||||
|
||||
@Test("Unkeyed decoding failures report exact errors", arguments: unkeyedDecodingErrorCases)
|
||||
func test_unkeyedDecodingFailuresReportExactErrors(testCase: UnkeyedDecodingErrorCase) {
|
||||
testCase.expectFailure()
|
||||
}
|
||||
|
||||
@Test("Unkeyed decoding validates before reading values")
|
||||
func test_unkeyedDecodingValidatesBeforeReadingValues() {
|
||||
let value = PureYAML.Model.Value.sequence([
|
||||
.mapping(.init([
|
||||
.init(key: "title", value: .string("First")),
|
||||
.init(key: "title", value: .string("Second")),
|
||||
])),
|
||||
])
|
||||
|
||||
expectValidationError {
|
||||
_ = try PureYAML.decode([TitleOnlyArticle].self, from: value)
|
||||
} check: { collection in
|
||||
#expect(collection.issues == [
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key 'title'",
|
||||
path: .init([.index(0), .key("title")]),
|
||||
),
|
||||
])
|
||||
#expect(collection.description == "error: Duplicate mapping key 'title' at $[0].title")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Direct unkeyed containers report overread paths exactly")
|
||||
func test_directUnkeyedContainersReportOverreadPathsExactly() throws {
|
||||
var container = try PureYAML.Decoding.Decoder(
|
||||
value: .sequence([.int(1)]),
|
||||
).unkeyedContainer()
|
||||
|
||||
#expect(try container.decode(Int.self) == 1)
|
||||
expectDecodingError(.valueNotFound(path: .init([.index(1)]))) {
|
||||
_ = try container.decode(Int.self)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Unkeyed encoding failures report exact errors")
|
||||
func test_unkeyedEncodingFailuresReportExactErrors() {
|
||||
expectEncodingError(
|
||||
.integerOutOfRange(type: "UInt64", path: .init([.index(0)])),
|
||||
) {
|
||||
_ = try PureYAML.encode([UInt64.max])
|
||||
}
|
||||
|
||||
expectEncodingError(
|
||||
.integerOutOfRange(type: "UInt64", path: .init([.index(0), .key("value")])),
|
||||
) {
|
||||
_ = try PureYAML.encode([WideInteger(value: UInt64.max)])
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Unkeyed coding error descriptions are exact")
|
||||
func test_unkeyedCodingErrorDescriptionsAreExact() {
|
||||
#expect(PureYAML.Decoding.Error.valueNotFound(
|
||||
path: .init([.index(1)]),
|
||||
).description == "No YAML value found at $[1]")
|
||||
|
||||
#expect(PureYAML.Decoding.Error.typeMismatch(
|
||||
expected: "sequence",
|
||||
actual: "string",
|
||||
path: .root,
|
||||
).description == "Expected sequence at $, found string")
|
||||
|
||||
#expect(PureYAML.Encoding.Error.integerOutOfRange(
|
||||
type: "UInt64",
|
||||
path: .init([.index(0)]),
|
||||
).description == "UInt64 value is outside PureYAML integer range at $[0]")
|
||||
}
|
||||
}
|
||||
|
||||
private let nestedIntegerValue = PureYAML.Model.Value.sequence([
|
||||
.sequence([.int(1), .int(2)]),
|
||||
.sequence([]),
|
||||
])
|
||||
|
||||
private let listedArticle = ListedArticle(
|
||||
title: "Typed YAML",
|
||||
tags: ["Swift", "YAML"],
|
||||
ranks: [1, nil, 3],
|
||||
)
|
||||
|
||||
private let listedArticleValue = PureYAML.Model.Value.mapping(.init([
|
||||
.init(key: "title", value: .string("Typed YAML")),
|
||||
.init(key: "tags", value: .sequence([
|
||||
.string("Swift"),
|
||||
.string("YAML"),
|
||||
])),
|
||||
.init(key: "ranks", value: .sequence([
|
||||
.int(1),
|
||||
.null,
|
||||
.int(3),
|
||||
])),
|
||||
]))
|
||||
|
||||
private let unkeyedDecodingErrorCases: [UnkeyedDecodingErrorCase] = [
|
||||
.init(
|
||||
name: "root value is not a sequence",
|
||||
value: .string("not a sequence"),
|
||||
expected: .typeMismatch(expected: "sequence", actual: "string", path: .root),
|
||||
kind: .integerArray,
|
||||
),
|
||||
.init(
|
||||
name: "element has wrong type",
|
||||
value: .sequence([.int(1), .string("two")]),
|
||||
expected: .typeMismatch(expected: "Int", actual: "string", path: .init([.index(1)])),
|
||||
kind: .integerArray,
|
||||
),
|
||||
.init(
|
||||
name: "nested element has wrong type",
|
||||
value: .sequence([
|
||||
.sequence([.int(1), .bool(false)]),
|
||||
]),
|
||||
expected: .typeMismatch(
|
||||
expected: "Int",
|
||||
actual: "bool",
|
||||
path: .init([.index(0), .index(1)]),
|
||||
),
|
||||
kind: .nestedIntegerArray,
|
||||
),
|
||||
.init(
|
||||
name: "keyed sequence property has wrong type",
|
||||
value: .mapping(.init([
|
||||
.init(key: "title", value: .string("Typed YAML")),
|
||||
.init(key: "tags", value: .string("Swift")),
|
||||
.init(key: "ranks", value: .sequence([])),
|
||||
])),
|
||||
expected: .typeMismatch(expected: "sequence", actual: "string", path: .init([.key("tags")])),
|
||||
kind: .listedArticle,
|
||||
),
|
||||
]
|
||||
|
||||
private struct ListedArticle: Codable, Equatable {
|
||||
var title: String
|
||||
var tags: [String]
|
||||
var ranks: [Int?]
|
||||
}
|
||||
|
||||
private struct TitleOnlyArticle: Codable, Equatable {
|
||||
var title: String
|
||||
}
|
||||
|
||||
private struct WideInteger: Encodable {
|
||||
var value: UInt64
|
||||
}
|
||||
|
||||
private struct SuperEncodedPair: Encodable {
|
||||
func encode(to encoder: any Encoder) throws {
|
||||
var container = encoder.unkeyedContainer()
|
||||
let first = container.superEncoder()
|
||||
let second = container.superEncoder()
|
||||
try "one".encode(to: first)
|
||||
try "two".encode(to: second)
|
||||
}
|
||||
}
|
||||
|
||||
struct UnkeyedDecodingErrorCase: CustomStringConvertible {
|
||||
enum Kind {
|
||||
case integerArray
|
||||
case nestedIntegerArray
|
||||
case listedArticle
|
||||
}
|
||||
|
||||
var name: String
|
||||
var value: PureYAML.Model.Value
|
||||
var expected: PureYAML.Decoding.Error
|
||||
var kind: Kind
|
||||
|
||||
var description: String {
|
||||
name
|
||||
}
|
||||
|
||||
func expectFailure() {
|
||||
do {
|
||||
switch kind {
|
||||
case .integerArray:
|
||||
_ = try PureYAML.decode([Int].self, from: value)
|
||||
case .nestedIntegerArray:
|
||||
_ = try PureYAML.decode([[Int]].self, from: value)
|
||||
case .listedArticle:
|
||||
_ = try PureYAML.decode(ListedArticle.self, from: value)
|
||||
}
|
||||
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 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)")
|
||||
}
|
||||
}
|
||||
@ -126,11 +126,11 @@ validation corpus pins exact issue paths, descriptions, severity splits,
|
||||
strict/non-strict behavior, rule traversal order, and duplicate-key diagnostics.
|
||||
|
||||
Typed conversion supports scalar single-value Decodable and Encodable values,
|
||||
plus keyed mapping-backed structs with scalar, optional scalar, and nested keyed
|
||||
struct fields. `PureYAML.decode(_:from:)`, `PureYAML.encode(_:)`, and
|
||||
`PureYAML.encodeToYAML(_:)` validate input shapes and throw exact path-aware
|
||||
typed coding errors. Unkeyed containers remain planned work under the typed
|
||||
conversion roadmap.
|
||||
keyed mapping-backed structs with scalar, optional scalar, nested keyed struct,
|
||||
and sequence fields, plus unkeyed sequences with scalar elements, optional
|
||||
elements, nested sequences, and keyed mapping elements. `PureYAML.decode(_:from:)`,
|
||||
`PureYAML.encode(_:)`, and `PureYAML.encodeToYAML(_:)` validate input shapes and
|
||||
throw exact path-aware typed coding errors.
|
||||
|
||||
Typed decoding runs default validation before Swift `Decodable` construction.
|
||||
That keeps ambiguous YAML states, such as duplicate mapping keys, from being
|
||||
@ -142,7 +142,7 @@ Compatibility should be added in small, test-backed slices:
|
||||
|
||||
1. Tag-aware collection handling and merge keys.
|
||||
2. Additional built-in validation rules beyond duplicate-key behavior.
|
||||
3. Unkeyed typed decoding and encoding, then broader Codable compatibility.
|
||||
3. Broader Codable compatibility beyond scalar, keyed, and unkeyed containers.
|
||||
4. Yams corpus comparison tests in a separate compatibility suite.
|
||||
|
||||
The private `PureYAMLResearch` repository may be used to study Yams behavior, but
|
||||
|
||||
Loading…
Reference in New Issue
Block a user