Build the frontmatter engine with byte-perfect round-trip
Yams validates and reads; writes are surgical line-span edits over retained raw text, so untouched bytes — unknown keys, comments, odd formatting, bodies — round-trip identically by construction. Strict schema/order readers distinguish valid/missing/malformed for the loader's fail-fast; lenient fields preserve malformed values verbatim; modified-by is schema-owned. Adds the Yams package and the fixture folder-reference wiring. 44 tests (105 cases). Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import Foundation
|
||||
import Yams
|
||||
|
||||
/// A `Sendable` snapshot of a parsed YAML value. Mappings keep document order; this is a
|
||||
/// read-only view — the engine never serializes through it (see `FrontmatterDocument`).
|
||||
public enum YAMLValue: Sendable, Equatable {
|
||||
case null
|
||||
case bool(Bool)
|
||||
case int(Int)
|
||||
case double(Double)
|
||||
case string(String)
|
||||
case date(Date)
|
||||
case sequence([YAMLValue])
|
||||
case mapping([Pair])
|
||||
|
||||
public struct Pair: Sendable, Equatable {
|
||||
public let key: YAMLValue
|
||||
public let value: YAMLValue
|
||||
|
||||
public init(key: YAMLValue, value: YAMLValue) {
|
||||
self.key = key
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension YAMLValue {
|
||||
init(_ node: Node, resolver: Resolver = .default) {
|
||||
switch node {
|
||||
case let .scalar(scalar):
|
||||
self = YAMLValue(scalar: scalar, tag: resolver.resolveTag(of: node))
|
||||
case let .sequence(sequence):
|
||||
self = .sequence(sequence.map { YAMLValue($0, resolver: resolver) })
|
||||
case let .mapping(mapping):
|
||||
self = .mapping(mapping.map {
|
||||
Pair(key: YAMLValue($0.key, resolver: resolver), value: YAMLValue($0.value, resolver: resolver))
|
||||
})
|
||||
case .alias:
|
||||
self = .null
|
||||
}
|
||||
}
|
||||
|
||||
private init(scalar: Node.Scalar, tag: Tag.Name) {
|
||||
switch tag {
|
||||
case .null:
|
||||
self = .null
|
||||
case .bool:
|
||||
self = Bool.construct(from: scalar).map(YAMLValue.bool) ?? .string(scalar.string)
|
||||
case .int:
|
||||
self = Int.construct(from: scalar).map(YAMLValue.int)
|
||||
?? Double.construct(from: scalar).map(YAMLValue.double)
|
||||
?? .string(scalar.string)
|
||||
case .float:
|
||||
self = Double.construct(from: scalar).map(YAMLValue.double) ?? .string(scalar.string)
|
||||
case .timestamp:
|
||||
self = Date.construct(from: scalar).map(YAMLValue.date) ?? .string(scalar.string)
|
||||
default:
|
||||
self = .string(scalar.string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension YAMLValue: CustomStringConvertible {
|
||||
/// Diagnostic rendering only — never used to write files.
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .null: "null"
|
||||
case let .bool(value): value ? "true" : "false"
|
||||
case let .int(value): String(value)
|
||||
case let .double(value): String(value)
|
||||
case let .string(value): value
|
||||
case let .date(value): value.formatted(.iso8601)
|
||||
case let .sequence(values): "[" + values.map(\.description).joined(separator: ", ") + "]"
|
||||
case let .mapping(pairs):
|
||||
"{" + pairs.map { "\($0.key.description): \($0.value.description)" }.joined(separator: ", ") + "}"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user