Files
lanework/Kanban/Storage/FrontmatterFields.swift
T
rzen cb6f6100a7 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
2026-07-26 15:26:12 -04:00

100 lines
3.6 KiB
Swift

import Foundation
import Yams
/// One top-level frontmatter entry: what YAML made of it plus the text as written.
public struct FrontmatterField: Sendable, Equatable {
public let key: String
public let value: YAMLValue
public let rawValue: String
}
/// The result of reading a typed field. `malformed` is what keeps strict fields (`schema`,
/// `order`) from being silently coerced and lenient fields (colors, icons, `width`) from
/// erroring — the loader decides which reaction each one gets.
public enum FieldValue<Value: Sendable & Equatable>: Sendable, Equatable {
case missing
case valid(Value)
case malformed(raw: String)
public var value: Value? {
if case let .valid(value) = self { return value }
return nil
}
public var isMissing: Bool {
if case .missing = self { return true }
return false
}
public var isMalformed: Bool {
if case .malformed = self { return true }
return false
}
/// The value as written, for a malformed field.
public var rawText: String? {
if case let .malformed(raw) = self { return raw }
return nil
}
}
extension FrontmatterDocument {
// MARK: - Strict (structure — the loader fails fast on `.malformed`)
public var schema: FieldValue<Int> {
read(FrontmatterKeys.schema) { if case let .int(value) = $0 { value } else { nil } }
}
public var order: FieldValue<Double> {
read(FrontmatterKeys.order) {
switch $0 {
case let .int(value): Double(value)
case let .double(value): value
default: nil
}
}
}
// MARK: - Lenient (a malformed value is preserved and simply not used)
public var title: FieldValue<String> { read(FrontmatterKeys.title, Self.string) }
public var background: FieldValue<String> { read(FrontmatterKeys.background, Self.string) }
public var icon: FieldValue<String> { read(FrontmatterKeys.icon, Self.string) }
public var iconColor: FieldValue<String> { read(FrontmatterKeys.iconColor, Self.string) }
/// Width multiplier; anything but an integer ≥ 1 is malformed and renders as the default 1.
public var width: FieldValue<Int> {
read(FrontmatterKeys.width) { if case let .int(value) = $0, value >= 1 { value } else { nil } }
}
public var created: FieldValue<Date> { read(FrontmatterKeys.created, Self.date) }
public var modified: FieldValue<Date> { read(FrontmatterKeys.modified, Self.date) }
public var deleted: FieldValue<Date> { read(FrontmatterKeys.deleted, Self.date) }
/// Schema-owned, not an unknown key: the app clears it on every app-mediated write.
public var modifiedBy: FieldValue<String> { read(FrontmatterKeys.modifiedBy, Self.string) }
// MARK: -
private func read<Value>(_ key: String, _ transform: (YAMLValue) -> Value?) -> FieldValue<Value> {
guard let value = value(for: key) else { return .missing }
if case .null = value { return .missing }
if let typed = transform(value) { return .valid(typed) }
return .malformed(raw: rawValue(for: key) ?? value.description)
}
private static func string(_ value: YAMLValue) -> String? {
if case let .string(text) = value { return text }
return nil
}
/// A quoted timestamp reads the same as an unquoted one — same YAML 1.1 timestamp grammar.
private static func date(_ value: YAMLValue) -> Date? {
switch value {
case let .date(date): date
case let .string(text): Date.construct(from: Node.Scalar(text))
default: nil
}
}
}