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
100 lines
3.6 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|