rawValue and the coercion/malformed paths now stop at the quote-aware inline-comment boundary, so 'title: 2048 # note' coerces to "2048" and a read-then-write no longer compounds the comment (pinned idempotent). Strictly read-side; round-trip bytes untouched. +6 tests (124 total). Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
965 lines
42 KiB
Swift
965 lines
42 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import Kanban
|
|
|
|
// MARK: - Fixtures
|
|
|
|
private enum Fixture {
|
|
static let rich = """
|
|
---
|
|
# board settings, hand-written
|
|
schema: 1
|
|
title: My Board
|
|
|
|
project: lanework # agent overlay
|
|
sphere: work
|
|
labels: [a, b, c]
|
|
template: {order: 3}
|
|
notes: |
|
|
line one
|
|
indented two
|
|
folded: >
|
|
wrapped
|
|
text
|
|
created: 2026-07-26T16:41:38Z
|
|
modified-by: claude
|
|
tagged: !!str 42
|
|
"quoted key": yes
|
|
# trailing note
|
|
---
|
|
Body text.
|
|
|
|
More body — with *markdown*.
|
|
"""
|
|
|
|
static let minimal = """
|
|
---
|
|
schema: 1
|
|
order: 1024
|
|
title: Thing
|
|
---
|
|
Body
|
|
"""
|
|
|
|
/// Verbatim copy of a real card written by an agent.
|
|
static let realCard = """
|
|
---
|
|
schema: 1
|
|
title: Build the frontmatter engine with byte-perfect round-trip
|
|
order: 3072
|
|
created: 2026-07-26T16:41:38Z
|
|
modified: 2026-07-26T19:06:55Z
|
|
modified-by: claude
|
|
source: DESIGN/01-storage-format.md
|
|
labels: [m1-storage-read]
|
|
---
|
|
Build the Frontmatter component.
|
|
**Design constraints:**
|
|
- index.md is YAML frontmatter between `---` delimiters, then a Markdown body.
|
|
|
|
"""
|
|
|
|
/// A flow mapping whose continuation sits at column 0 — the shape that would fool a naive
|
|
/// "any `key:` at column 0 starts a field" scanner.
|
|
static let unindentedFlow = """
|
|
---
|
|
flow: {a: 1,
|
|
b: 2}
|
|
last: x
|
|
---
|
|
body
|
|
"""
|
|
|
|
/// A hand-edited file that says `title` twice — strict YAML would reject it; the editor reads
|
|
/// the last one and keeps both on disk (01-storage-format.md § Frontmatter).
|
|
static let duplicated = """
|
|
---
|
|
schema: 1
|
|
title: First
|
|
order: 1024
|
|
title: Second
|
|
---
|
|
body
|
|
|
|
"""
|
|
}
|
|
|
|
private func parseError(_ text: String) -> FrontmatterError? {
|
|
do {
|
|
_ = try FrontmatterDocument.parse(text)
|
|
return nil
|
|
} catch {
|
|
return error
|
|
}
|
|
}
|
|
|
|
// MARK: - Round-trip
|
|
|
|
struct FrontmatterRoundTripTests {
|
|
@Test(arguments: [
|
|
Fixture.rich,
|
|
Fixture.minimal,
|
|
Fixture.realCard,
|
|
Fixture.unindentedFlow,
|
|
// body with a trailing newline
|
|
"---\nschema: 1\n---\nbody\n",
|
|
// body with several trailing newlines
|
|
"---\nschema: 1\n---\nbody\n\n\n",
|
|
// no body at all, closing delimiter unterminated
|
|
"---\nschema: 1\n---",
|
|
// no body, closing delimiter terminated
|
|
"---\nschema: 1\n---\n",
|
|
// empty frontmatter mapping
|
|
"---\n---\nbody\n",
|
|
// frontmatter that is only blank lines
|
|
"---\n\n\n---\nbody\n",
|
|
// frontmatter that is only comments
|
|
"---\n# nothing to see\n---\nbody\n",
|
|
// explicit empty flow mapping
|
|
"---\n{}\n---\nbody\n",
|
|
// top-level flow mapping
|
|
"---\n{schema: 1, order: 5}\n---\nbody\n",
|
|
// odd key order, blank lines between entries
|
|
"---\nzzz: last\n\norder: 5\n\n\nschema: 1\n---\nbody\n",
|
|
// block scalar keep/strip indicators
|
|
"---\nkeep: |+\n text\n\nstrip: |-\n text\n---\nbody\n",
|
|
// anchors and aliases
|
|
"---\nbase: &b\n x: 1\ncopy: *b\n---\nbody\n",
|
|
// quoted values with escapes and embedded delimiters
|
|
"---\ntitle: \"a \\\"quoted\\\" --- thing\"\nother: 'single ---'\n---\nbody\n",
|
|
// a --- inside a block scalar (indented, so not a delimiter)
|
|
"---\nnote: |\n ---\n still the value\n---\nbody\n",
|
|
// delimiter line with trailing spaces
|
|
"--- \nschema: 1\n--- \nbody\n",
|
|
// no trailing newline on the body
|
|
"---\nschema: 1\n---\nbody without newline",
|
|
// CRLF throughout
|
|
"---\r\nschema: 1\r\n---\r\nbody\r\n",
|
|
// duplicate top-level keys — every occurrence keeps its own span
|
|
Fixture.duplicated,
|
|
"---\ntitle: a\ntitle: b\ntitle: c\n---\nbody\n",
|
|
// a duplicate whose earlier twin owns a comment and a block scalar
|
|
"---\nnotes: |\n one\n# about notes\nnotes: two\n---\nbody\n",
|
|
])
|
|
func serializingAnUntouchedDocumentIsByteIdentical(text: String) throws {
|
|
let document = try FrontmatterDocument.parse(text)
|
|
#expect(document.serialized() == text)
|
|
}
|
|
|
|
@Test func bodyIsEverythingAfterTheClosingDelimiter() throws {
|
|
let document = try FrontmatterDocument.parse(Fixture.rich)
|
|
#expect(document.body == "Body text.\n\nMore body — with *markdown*.")
|
|
}
|
|
|
|
@Test func emptyBodyIsEmpty() throws {
|
|
#expect(try FrontmatterDocument.parse("---\nschema: 1\n---\n").body == "")
|
|
#expect(try FrontmatterDocument.parse("---\nschema: 1\n---").body == "")
|
|
}
|
|
|
|
@Test func bodyTrailingNewlinesAreNotNormalized() throws {
|
|
let document = try FrontmatterDocument.parse("---\nschema: 1\n---\nbody\n\n\n")
|
|
#expect(document.body == "body\n\n\n")
|
|
}
|
|
}
|
|
|
|
// MARK: - Malformed input
|
|
|
|
struct FrontmatterMalformedTests {
|
|
@Test func missingOpeningDelimiter() {
|
|
#expect(parseError("schema: 1\n---\nbody\n") == .missingOpeningDelimiter)
|
|
#expect(parseError("") == .missingOpeningDelimiter)
|
|
#expect(parseError("\n---\nschema: 1\n---\n") == .missingOpeningDelimiter)
|
|
#expect(parseError("----\nschema: 1\n---\n") == .missingOpeningDelimiter)
|
|
#expect(parseError("--- yaml\nschema: 1\n---\n") == .missingOpeningDelimiter)
|
|
}
|
|
|
|
@Test func missingClosingDelimiter() {
|
|
#expect(parseError("---\nschema: 1\nbody\n") == .missingClosingDelimiter)
|
|
#expect(parseError("---\n") == .missingClosingDelimiter)
|
|
#expect(parseError("---") == .missingClosingDelimiter)
|
|
}
|
|
|
|
@Test(arguments: [
|
|
"---\nfoo: [1, 2\n---\nbody\n",
|
|
"---\nschema: 1\n\tindented-with-tab: 2\n---\nbody\n",
|
|
"---\nschema: 1\n bad: indent\n---\nbody\n",
|
|
"---\n\"unterminated: 1\n---\nbody\n",
|
|
"---\nfoo: {a: 1\n---\nbody\n",
|
|
"---\n*undefined-alias\n---\nbody\n",
|
|
// last-wins rescues duplicates at the top level only — nested ones stay a hard failure
|
|
"---\nouter:\n a: 1\n a: 2\n---\nbody\n",
|
|
"---\nflow: {a: 1, a: 2}\n---\nbody\n",
|
|
"---\ndupe: 1\ndupe: 2\nouter:\n a: 1\n a: 2\n---\nbody\n",
|
|
])
|
|
func unparseableYAML(text: String) {
|
|
guard case .unparseableYAML = parseError(text) else {
|
|
Issue.record("expected .unparseableYAML for \(text.debugDescription), got \(String(describing: parseError(text)))")
|
|
return
|
|
}
|
|
}
|
|
|
|
@Test func unparseableYAMLCarriesAFileRelativeLineNumber() {
|
|
guard case let .unparseableYAML(message, line) = parseError("---\nschema: 1\n bad: indent\n---\n") else {
|
|
Issue.record("expected .unparseableYAML")
|
|
return
|
|
}
|
|
#expect(line == 3)
|
|
#expect(!message.isEmpty)
|
|
}
|
|
|
|
@Test(arguments: [
|
|
"---\n- a\n- b\n---\nbody\n",
|
|
"---\njust a scalar\n---\nbody\n",
|
|
"---\n42\n---\nbody\n",
|
|
"---\n[1, 2]\n---\nbody\n",
|
|
])
|
|
func frontmatterNotAMapping(text: String) {
|
|
#expect(parseError(text) == .frontmatterNotAMapping)
|
|
}
|
|
|
|
@Test(arguments: [
|
|
"---\n---\nbody\n",
|
|
"---\n\n---\nbody\n",
|
|
"---\n# just a comment\n---\nbody\n",
|
|
"---\n{}\n---\nbody\n",
|
|
"---\nnull\n---\nbody\n",
|
|
"---\n~\n---\nbody\n",
|
|
])
|
|
func emptyFrontmatterIsAnEmptyMapping(text: String) throws {
|
|
let document = try FrontmatterDocument.parse(text)
|
|
#expect(document.keys.isEmpty)
|
|
#expect(document.serialized() == text)
|
|
}
|
|
|
|
@Test func wellFormedButUnusualYAMLIsAccepted() throws {
|
|
for text in [Fixture.rich, Fixture.unindentedFlow, "---\nbase: &b\n x: 1\ncopy: *b\n---\n"] {
|
|
#expect(parseError(text) == nil)
|
|
}
|
|
}
|
|
|
|
/// Strict YAML rejects a repeated mapping key; the editor deliberately does not.
|
|
@Test func duplicateTopLevelKeysAreNotAParseError() throws {
|
|
for text in [
|
|
Fixture.duplicated,
|
|
"---\ndupe: 1\ndupe: 2\n---\nbody\n",
|
|
"---\ntitle: a\ntitle: b\ntitle: c\n---\nbody\n",
|
|
] {
|
|
#expect(parseError(text) == nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Duplicate keys (last one wins)
|
|
|
|
/// 01-storage-format.md § Frontmatter: a key written twice reads as its last occurrence, the
|
|
/// earlier ones preserved verbatim on disk and invisible; an app write of that key rewrites the
|
|
/// winner and removes the twins; a removal removes them all.
|
|
struct FrontmatterDuplicateKeyTests {
|
|
@Test func theLastOccurrenceIsTheOneThatReads() throws {
|
|
let document = try FrontmatterDocument.parse(Fixture.duplicated)
|
|
#expect(document.title == .valid("Second"))
|
|
#expect(document.value(for: "title") == .string("Second"))
|
|
#expect(document.rawValue(for: "title") == "Second")
|
|
#expect(document.contains("title"))
|
|
}
|
|
|
|
@Test func threeOccurrencesStillReadAsTheLast() throws {
|
|
let document = try FrontmatterDocument.parse("---\ntitle: a\ntitle: b\ntitle: c\n---\nbody\n")
|
|
#expect(document.title == .valid("c"))
|
|
#expect(document.keys == ["title"])
|
|
}
|
|
|
|
/// The effective view has one entry per key, sitting where the winner sits — the order the
|
|
/// file itself takes once `set` collapses the twins.
|
|
@Test func iterationPresentsTheEffectiveView() throws {
|
|
let document = try FrontmatterDocument.parse(Fixture.duplicated)
|
|
#expect(document.keys == ["schema", "order", "title"])
|
|
#expect(document.fields.map(\.key) == ["schema", "order", "title"])
|
|
#expect(document.fields.last?.rawValue == "Second")
|
|
}
|
|
|
|
@Test func bothOccurrencesSurviveAnUntouchedRoundTrip() throws {
|
|
let document = try FrontmatterDocument.parse(Fixture.duplicated)
|
|
let output = document.serialized()
|
|
#expect(output == Fixture.duplicated)
|
|
#expect(output.contains("title: First"))
|
|
#expect(output.contains("title: Second"))
|
|
}
|
|
|
|
@Test func settingRewritesTheWinnerAndDeletesTheTwins() throws {
|
|
var document = try FrontmatterDocument.parse(Fixture.duplicated)
|
|
document.set("title", to: .string("Third"))
|
|
#expect(document.serialized() == "---\nschema: 1\norder: 1024\ntitle: Third\n---\nbody\n")
|
|
#expect(document.title == .valid("Third"))
|
|
}
|
|
|
|
@Test func settingCollapsesThreeOccurrencesToOne() throws {
|
|
var document = try FrontmatterDocument.parse("---\ntitle: a\ntitle: b\ntitle: c\n---\nbody\n")
|
|
document.set("title", to: .string("d"))
|
|
#expect(document.serialized() == "---\ntitle: d\n---\nbody\n")
|
|
}
|
|
|
|
/// A stale twin left behind would resurrect itself the moment the winner were removed.
|
|
@Test func removingTakesEveryOccurrence() throws {
|
|
var document = try FrontmatterDocument.parse(Fixture.duplicated)
|
|
document.remove("title")
|
|
#expect(document.serialized() == "---\nschema: 1\norder: 1024\n---\nbody\n")
|
|
#expect(document.title == .missing)
|
|
#expect(!document.contains("title"))
|
|
#expect(document.keys == ["schema", "order"])
|
|
}
|
|
|
|
@Test func removingTakesAllThreeOccurrences() throws {
|
|
var document = try FrontmatterDocument.parse("---\nschema: 1\ntitle: a\ntitle: b\ntitle: c\n---\nbody\n")
|
|
document.remove("title")
|
|
#expect(document.serialized() == "---\nschema: 1\n---\nbody\n")
|
|
}
|
|
|
|
/// Own-line comments outlive the twin they were written above (01-storage-format.md).
|
|
@Test func aCommentAboveTheFirstOccurrenceSurvivesTheCollapse() throws {
|
|
let text = "---\n# the original title\ntitle: First\ntitle: Second\n---\nbody\n"
|
|
var document = try FrontmatterDocument.parse(text)
|
|
#expect(document.serialized() == text)
|
|
|
|
document.set("title", to: .string("Third"))
|
|
#expect(document.serialized() == "---\n# the original title\ntitle: Third\n---\nbody\n")
|
|
}
|
|
|
|
/// Same rule when the comment trails the earlier twin rather than heading it.
|
|
@Test func aCommentBelowTheFirstOccurrenceSurvivesTheCollapse() throws {
|
|
var document = try FrontmatterDocument.parse("---\ntitle: First\n# still relevant\ntitle: Second\n---\nbody\n")
|
|
document.remove("title")
|
|
#expect(document.serialized() == "---\n# still relevant\n---\nbody\n")
|
|
}
|
|
|
|
@Test func twoDifferentKeysMayEachBeDuplicated() throws {
|
|
let text = "---\na: 1\nb: 1\na: 2\nb: 2\n---\nbody\n"
|
|
var document = try FrontmatterDocument.parse(text)
|
|
#expect(document.serialized() == text)
|
|
#expect(document.value(for: "a") == .int(2))
|
|
#expect(document.value(for: "b") == .int(2))
|
|
#expect(document.keys == ["a", "b"])
|
|
|
|
document.set("a", to: .int(3))
|
|
#expect(document.serialized() == "---\nb: 1\na: 3\nb: 2\n---\nbody\n")
|
|
}
|
|
|
|
/// A duplicated twin whose value is a block scalar still spans its own lines.
|
|
@Test func aDuplicateWithAMultiLineTwinCollapsesCleanly() throws {
|
|
let text = "---\nnotes: |\n one\n# about notes\nnotes: two\n---\nbody\n"
|
|
var document = try FrontmatterDocument.parse(text)
|
|
#expect(document.serialized() == text)
|
|
#expect(document.value(for: "notes") == .string("two"))
|
|
|
|
document.set("notes", to: .string("three"))
|
|
#expect(document.serialized() == "---\n# about notes\nnotes: three\n---\nbody\n")
|
|
}
|
|
|
|
/// The twins need not be spelled alike — YAML reads `"title"` and `title` as the same key,
|
|
/// and each occurrence is rewritten (or dropped) in its own spelling.
|
|
@Test func twinsSpelledDifferentlyAreStillTheSameKey() throws {
|
|
let text = "---\n\"title\": First\ntitle: Second\n---\nbody\n"
|
|
var document = try FrontmatterDocument.parse(text)
|
|
#expect(document.serialized() == text)
|
|
#expect(document.title == .valid("Second"))
|
|
|
|
document.set("title", to: .string("Third"))
|
|
#expect(document.serialized() == "---\ntitle: Third\n---\nbody\n")
|
|
}
|
|
|
|
@Test func duplicatesInACRLFFileRoundTripAndCollapse() throws {
|
|
let text = "---\r\ntitle: First\r\norder: 1\r\ntitle: Second\r\n---\r\nbody\r\n"
|
|
var document = try FrontmatterDocument.parse(text)
|
|
#expect(document.serialized() == text)
|
|
#expect(document.title == .valid("Second"))
|
|
|
|
document.set("title", to: .string("Third"))
|
|
#expect(document.serialized() == "---\r\norder: 1\r\ntitle: Third\r\n---\r\nbody\r\n")
|
|
}
|
|
|
|
@Test func duplicatesSurviveReparsing() throws {
|
|
var document = try FrontmatterDocument.parse(Fixture.duplicated)
|
|
document.set("order", to: .double(2048))
|
|
let reparsed = try FrontmatterDocument.parse(document.serialized())
|
|
#expect(reparsed.serialized() == document.serialized())
|
|
#expect(reparsed.title == .valid("Second"))
|
|
#expect(reparsed.order == .valid(2048))
|
|
}
|
|
}
|
|
|
|
// MARK: - Generic access
|
|
|
|
struct FrontmatterAccessTests {
|
|
@Test func keysAreInDocumentOrder() throws {
|
|
let document = try FrontmatterDocument.parse(Fixture.rich)
|
|
#expect(document.keys == [
|
|
"schema", "title", "project", "sphere", "labels", "template",
|
|
"notes", "folded", "created", "modified-by", "tagged", "quoted key",
|
|
])
|
|
}
|
|
|
|
@Test func realCardReadsAsExpected() throws {
|
|
let document = try FrontmatterDocument.parse(Fixture.realCard)
|
|
#expect(document.schema == .valid(1))
|
|
#expect(document.order == .valid(3072))
|
|
#expect(document.title == .valid("Build the frontmatter engine with byte-perfect round-trip"))
|
|
#expect(document.modifiedBy == .valid("claude"))
|
|
#expect(document.created.value != nil)
|
|
#expect(document.unknownFields.map(\.key) == ["source", "labels"])
|
|
#expect(document.body.hasPrefix("Build the Frontmatter component.\n"))
|
|
}
|
|
|
|
@Test func unknownKeysExcludeSchemaOwnedOnes() throws {
|
|
let document = try FrontmatterDocument.parse(Fixture.rich)
|
|
#expect(document.unknownFields.map(\.key) == [
|
|
"project", "sphere", "labels", "template", "notes", "folded", "tagged", "quoted key",
|
|
])
|
|
}
|
|
|
|
@Test func modifiedByIsExposedDistinctlyAndIsNotAnUnknownKey() throws {
|
|
let document = try FrontmatterDocument.parse(Fixture.rich)
|
|
#expect(document.modifiedBy == .valid("claude"))
|
|
#expect(!document.unknownFields.contains { $0.key == "modified-by" })
|
|
#expect(document.contains("modified-by"))
|
|
}
|
|
|
|
@Test func reservedKeysAreOrdinaryUnknownKeys() throws {
|
|
let text = """
|
|
---
|
|
schema: 1
|
|
labels: [bug, ui]
|
|
assignees: [ann]
|
|
due: 2026-08-01
|
|
remote: {kind: gitea}
|
|
remote-state: synced
|
|
template: {order: 2}
|
|
---
|
|
body
|
|
|
|
"""
|
|
let document = try FrontmatterDocument.parse(text)
|
|
#expect(document.unknownFields.map(\.key)
|
|
== ["labels", "assignees", "due", "remote", "remote-state", "template"])
|
|
#expect(document.serialized() == text)
|
|
}
|
|
|
|
@Test func parsedValuesAreAvailableForUnknownKeys() throws {
|
|
let document = try FrontmatterDocument.parse(Fixture.rich)
|
|
#expect(document.value(for: "labels") == .sequence([.string("a"), .string("b"), .string("c")]))
|
|
#expect(document.value(for: "template")
|
|
== .mapping([YAMLValue.Pair(key: .string("order"), value: .int(3))]))
|
|
#expect(document.value(for: "quoted key") == .bool(true))
|
|
#expect(document.value(for: "tagged") == .string("42"))
|
|
#expect(document.value(for: "nope") == nil)
|
|
}
|
|
|
|
@Test func rawValuePreservesBlockScalarsVerbatim() throws {
|
|
let document = try FrontmatterDocument.parse(Fixture.rich)
|
|
#expect(document.rawValue(for: "notes") == "|\n line one\n indented two")
|
|
#expect(document.rawValue(for: "labels") == "[a, b, c]")
|
|
#expect(document.rawValue(for: "schema") == "1")
|
|
}
|
|
|
|
/// A trailing comment belongs to the line, not to the value — reads stop at it.
|
|
@Test func rawValueStopsAtAnInlineComment() throws {
|
|
let document = try FrontmatterDocument.parse(Fixture.rich)
|
|
#expect(document.rawValue(for: "project") == "lanework")
|
|
}
|
|
|
|
/// …but a `#` inside a quoted scalar is value text, and a multi-line value is never scanned.
|
|
@Test func rawValueKeepsAHashThatIsNotAComment() throws {
|
|
let text = """
|
|
---
|
|
quoted: "hash # inside"
|
|
single: 'hash # inside'
|
|
both: "hash # inside" # and a real one
|
|
flow: [a, "b # c"] # note
|
|
block: |
|
|
text # not a comment out here either
|
|
---
|
|
body
|
|
|
|
"""
|
|
let document = try FrontmatterDocument.parse(text)
|
|
#expect(document.rawValue(for: "quoted") == "\"hash # inside\"")
|
|
#expect(document.rawValue(for: "single") == "'hash # inside'")
|
|
#expect(document.rawValue(for: "both") == "\"hash # inside\"")
|
|
#expect(document.rawValue(for: "flow") == "[a, \"b # c\"]")
|
|
#expect(document.rawValue(for: "block") == "|\n text # not a comment out here either")
|
|
#expect(document.serialized() == text)
|
|
}
|
|
}
|
|
|
|
// MARK: - Strict fields
|
|
|
|
struct FrontmatterStrictFieldTests {
|
|
private func document(_ frontmatter: String) throws -> FrontmatterDocument {
|
|
try FrontmatterDocument.parse("---\n\(frontmatter)\n---\nbody\n")
|
|
}
|
|
|
|
@Test func schemaValidMissingMalformed() throws {
|
|
#expect(try document("schema: 1").schema == .valid(1))
|
|
#expect(try document("title: x").schema == .missing)
|
|
#expect(try document("schema:").schema == .missing)
|
|
#expect(try document("schema: banana").schema == .malformed(raw: "banana"))
|
|
#expect(try document("schema: 1.5").schema == .malformed(raw: "1.5"))
|
|
#expect(try document("schema: [1]").schema == .malformed(raw: "[1]"))
|
|
}
|
|
|
|
@Test func orderValidMissingMalformed() throws {
|
|
#expect(try document("order: 1024").order == .valid(1024))
|
|
#expect(try document("order: 1536.5").order == .valid(1536.5))
|
|
#expect(try document("order: -1024").order == .valid(-1024))
|
|
#expect(try document("schema: 1").order == .missing)
|
|
#expect(try document("order:").order == .missing)
|
|
}
|
|
|
|
@Test func nonNumericOrderIsMalformedNotNil() throws {
|
|
let malformed = try document("order: banana").order
|
|
#expect(malformed == .malformed(raw: "banana"))
|
|
#expect(malformed.value == nil)
|
|
#expect(malformed.isMalformed)
|
|
#expect(!malformed.isMissing)
|
|
#expect(malformed.rawText == "banana")
|
|
|
|
#expect(try document("order: \"1024\"").order == .malformed(raw: "\"1024\""))
|
|
#expect(try document("order: [1]").order == .malformed(raw: "[1]"))
|
|
}
|
|
}
|
|
|
|
// MARK: - Lenient fields
|
|
|
|
struct FrontmatterLenientFieldTests {
|
|
private func document(_ frontmatter: String) throws -> FrontmatterDocument {
|
|
try FrontmatterDocument.parse("---\n\(frontmatter)\n---\nbody\n")
|
|
}
|
|
|
|
@Test func wellFormedLenientValues() throws {
|
|
#expect(try document("title: My Board").title == .valid("My Board"))
|
|
#expect(try document("background: \"#ff8800\"").background == .valid("#ff8800"))
|
|
#expect(try document("background: slate").background == .valid("slate"))
|
|
#expect(try document("icon: tray.full").icon == .valid("tray.full"))
|
|
#expect(try document("iconColor: teal").iconColor == .valid("teal"))
|
|
#expect(try document("width: 3").width == .valid(3))
|
|
#expect(try document("width: 1").width == .valid(1))
|
|
}
|
|
|
|
/// A scalar of the wrong YAML type still has a sensible string reading — it coerces to the
|
|
/// source text the author typed (01-storage-format.md § Frontmatter). Only a sequence or
|
|
/// mapping — no scalar to read at all — is malformed.
|
|
@Test func scalarsOfTheWrongTypeCoerceToTheirSourceText() throws {
|
|
#expect(try document("title: 2048").title == .valid("2048"))
|
|
#expect(try document("title: true").title == .valid("true"))
|
|
#expect(try document("background: 42").background == .valid("42"))
|
|
#expect(try document("iconColor: true").iconColor == .valid("true"))
|
|
#expect(try document("icon: 2026-07-26T16:41:38Z").icon == .valid("2026-07-26T16:41:38Z"))
|
|
}
|
|
|
|
/// The comment on a value line belongs to the line, not to the value: the source text a
|
|
/// wrong-typed scalar coerces to stops where the comment starts.
|
|
@Test func aTrailingCommentIsNotPartOfACoercedValue() throws {
|
|
#expect(try document("title: 2048 # note").title == .valid("2048"))
|
|
#expect(try document("title: true # note").title == .valid("true"))
|
|
#expect(try document("background: 42\t# tabbed").background == .valid("42"))
|
|
#expect(try document("icon: 2026-07-26T16:41:38Z # when").icon == .valid("2026-07-26T16:41:38Z"))
|
|
}
|
|
|
|
/// A `#` inside a quoted scalar is value text — the quotes are what make `#ff8800` writable
|
|
/// at all, so a read must not treat it as a comment.
|
|
@Test func aHashInsideAQuotedValueIsNotTrimmed() throws {
|
|
#expect(try document("title: \"2048 # note\"").title == .valid("2048 # note"))
|
|
#expect(try document("background: \"#ff8800\"").background == .valid("#ff8800"))
|
|
#expect(try document("background: \"#ff8800\" # brand orange").background == .valid("#ff8800"))
|
|
}
|
|
|
|
/// A sequence or mapping has no scalar reading at all; the raw text it falls back to stops at
|
|
/// the comment like every other read.
|
|
@Test func malformedRawStopsAtAnInlineComment() throws {
|
|
#expect(try document("background: [red, blue] # a palette").background == .malformed(raw: "[red, blue]"))
|
|
#expect(try document("title: {a: 1} # a mapping").title == .malformed(raw: "{a: 1}"))
|
|
#expect(try document("width: wide # roughly").width == .malformed(raw: "wide"))
|
|
#expect(try document("order: banana # not a number").order == .malformed(raw: "banana"))
|
|
}
|
|
|
|
@Test func quotedStringLenientValuesAreUnaffectedByCoercion() throws {
|
|
#expect(try document("title: \"2048\"").title == .valid("2048"))
|
|
#expect(try document("title: \"true\"").title == .valid("true"))
|
|
#expect(try document("title: My Board").title == .valid("My Board"))
|
|
}
|
|
|
|
@Test func malformedLenientValuesArePreservedVerbatimAndDoNotThrow() throws {
|
|
#expect(try document("background: [red, blue]").background == .malformed(raw: "[red, blue]"))
|
|
#expect(try document("icon: {a: 1}").icon == .malformed(raw: "{a: 1}"))
|
|
#expect(try document("title: [a, b]").title == .malformed(raw: "[a, b]"))
|
|
}
|
|
|
|
@Test func widthIsLenientForAnythingButAnIntegerAtLeastOne() throws {
|
|
#expect(try document("width: 0").width == .malformed(raw: "0"))
|
|
#expect(try document("width: -3").width == .malformed(raw: "-3"))
|
|
#expect(try document("width: 1.5").width == .malformed(raw: "1.5"))
|
|
#expect(try document("width: wide").width == .malformed(raw: "wide"))
|
|
#expect(try document("schema: 1").width == .missing)
|
|
}
|
|
|
|
/// A string or double with an exact integer reading ≥ 1 coerces; a fractional reading,
|
|
/// zero, a negative, or non-numeric text still has none.
|
|
@Test func widthCoercesStringsAndWholeNumberDoubles() throws {
|
|
#expect(try document("width: \"2\"").width == .valid(2))
|
|
#expect(try document("width: 2.0").width == .valid(2))
|
|
#expect(try document("width: 2.7").width == .malformed(raw: "2.7"))
|
|
#expect(try document("width: 0").width == .malformed(raw: "0"))
|
|
#expect(try document("width: -1").width == .malformed(raw: "-1"))
|
|
#expect(try document("width: banana").width == .malformed(raw: "banana"))
|
|
}
|
|
|
|
@Test func malformedLenientValuesStillRoundTrip() throws {
|
|
let text = "---\nschema: 1\nbackground: [red, blue]\nwidth: 0\nicon: {a: 1}\n---\nbody\n"
|
|
let document = try FrontmatterDocument.parse(text)
|
|
#expect(document.serialized() == text)
|
|
#expect(document.background.isMalformed)
|
|
#expect(document.width.isMalformed)
|
|
#expect(document.icon.isMalformed)
|
|
}
|
|
|
|
@Test func timestampsReadPlainAndQuoted() throws {
|
|
let expected = Date(timeIntervalSince1970: 1_785_084_098) // 2026-07-26T16:41:38Z
|
|
#expect(try document("created: 2026-07-26T16:41:38Z").created == .valid(expected))
|
|
#expect(try document("modified: \"2026-07-26T16:41:38Z\"").modified == .valid(expected))
|
|
#expect(try document("deleted: 2026-07-26T16:41:38Z").deleted == .valid(expected))
|
|
#expect(try document("schema: 1").deleted == .missing)
|
|
#expect(try document("created: never").created == .malformed(raw: "never"))
|
|
}
|
|
|
|
/// A quoted ISO-8601 string is not YAML's implicit timestamp type — it parses as `.string`
|
|
/// — but still coerces to a valid date (01-storage-format.md § Frontmatter).
|
|
@Test func quotedISO8601StringCoercesToAValidDate() throws {
|
|
let expected = Date(timeIntervalSince1970: 1_767_323_045) // 2026-01-02T03:04:05Z
|
|
#expect(try document("created: \"2026-01-02T03:04:05Z\"").created == .valid(expected))
|
|
}
|
|
}
|
|
|
|
// MARK: - Surgical edits
|
|
|
|
struct FrontmatterEditTests {
|
|
@Test func settingOneKeyLeavesEveryOtherByteIdentical() throws {
|
|
var document = try FrontmatterDocument.parse(Fixture.rich)
|
|
document.set("title", to: .string("Renamed"))
|
|
let output = document.serialized()
|
|
|
|
let expected = Fixture.rich.replacingOccurrences(of: "title: My Board", with: "title: Renamed")
|
|
#expect(output == expected)
|
|
|
|
// Prefix and suffix around the edited span are untouched.
|
|
let marker = "title: "
|
|
let originalPrefix = Fixture.rich.prefix(while: { _ in true }).components(separatedBy: marker)[0]
|
|
#expect(output.hasPrefix(originalPrefix))
|
|
#expect(output.hasSuffix("\nMore body — with *markdown*."))
|
|
}
|
|
|
|
@Test func settingAKeyPreservesItsTrailingCommentsAndBlankLines() throws {
|
|
let text = "---\nschema: 1\nproject: lanework # agent overlay\n\n# a note\norder: 5\n---\nbody\n"
|
|
var document = try FrontmatterDocument.parse(text)
|
|
document.set("project", to: .string("kanban"))
|
|
#expect(document.serialized()
|
|
== "---\nschema: 1\nproject: kanban # agent overlay\n\n# a note\norder: 5\n---\nbody\n")
|
|
}
|
|
|
|
@Test func appendingANewKeyGoesBeforeTheClosingDelimiter() throws {
|
|
var document = try FrontmatterDocument.parse(Fixture.minimal)
|
|
document.set("modified", to: .date(Date(timeIntervalSince1970: 1_785_084_098)))
|
|
#expect(document.serialized() == """
|
|
---
|
|
schema: 1
|
|
order: 1024
|
|
title: Thing
|
|
modified: 2026-07-26T16:41:38Z
|
|
---
|
|
Body
|
|
""")
|
|
#expect(document.keys == ["schema", "order", "title", "modified"])
|
|
}
|
|
|
|
@Test func appendingToEmptyFrontmatter() throws {
|
|
var document = try FrontmatterDocument.parse("---\n---\nbody\n")
|
|
document.set("schema", to: .int(1))
|
|
#expect(document.serialized() == "---\nschema: 1\n---\nbody\n")
|
|
#expect(document.schema == .valid(1))
|
|
}
|
|
|
|
@Test func removingAKeyDeletesOnlyItsLines() throws {
|
|
var document = try FrontmatterDocument.parse(Fixture.minimal)
|
|
document.remove("order")
|
|
#expect(document.serialized() == "---\nschema: 1\ntitle: Thing\n---\nBody")
|
|
#expect(document.order == .missing)
|
|
#expect(document.keys == ["schema", "title"])
|
|
}
|
|
|
|
@Test func removingAKeyKeepsTrailingComments() throws {
|
|
var document = try FrontmatterDocument.parse("---\na: 1\n# note about b\nb: 2\n---\nbody\n")
|
|
document.remove("a")
|
|
#expect(document.serialized() == "---\n# note about b\nb: 2\n---\nbody\n")
|
|
}
|
|
|
|
@Test func removingAnAbsentKeyIsANoOp() throws {
|
|
var document = try FrontmatterDocument.parse(Fixture.minimal)
|
|
document.remove("nope")
|
|
#expect(document.serialized() == Fixture.minimal)
|
|
}
|
|
|
|
@Test func editingAKeyWhoseValueWasABlockScalar() throws {
|
|
var document = try FrontmatterDocument.parse(Fixture.rich)
|
|
document.set("notes", to: .string("flattened"))
|
|
#expect(document.serialized().contains("notes: flattened\nfolded: >\n"))
|
|
#expect(!document.serialized().contains("line one"))
|
|
#expect(document.value(for: "notes") == .string("flattened"))
|
|
// Everything after the edited span is untouched.
|
|
#expect(document.serialized().hasSuffix("More body — with *markdown*."))
|
|
}
|
|
|
|
@Test func editingAKeyAfterAnUnindentedFlowContinuation() throws {
|
|
var document = try FrontmatterDocument.parse(Fixture.unindentedFlow)
|
|
document.set("last", to: .string("y"))
|
|
#expect(document.serialized() == "---\nflow: {a: 1,\nb: 2}\nlast: y\n---\nbody")
|
|
}
|
|
|
|
@Test func editingAQuotedKeyKeepsItsOriginalSpelling() throws {
|
|
var document = try FrontmatterDocument.parse("---\n\"quoted key\": one\n---\nbody\n")
|
|
document.set("quoted key", to: .string("two"))
|
|
#expect(document.serialized() == "---\n\"quoted key\": two\n---\nbody\n")
|
|
}
|
|
|
|
@Test func buildingADocumentFromScratch() throws {
|
|
var document = FrontmatterDocument(body: "The card body.\n")
|
|
document.set("schema", to: .int(1))
|
|
document.set("order", to: .double(1024))
|
|
document.set("title", to: .string("New card"))
|
|
let text = document.serialized()
|
|
#expect(text == "---\nschema: 1\norder: 1024\ntitle: New card\n---\nThe card body.\n")
|
|
#expect(try FrontmatterDocument.parse(text).serialized() == text)
|
|
}
|
|
|
|
@Test func settingTheBodyReplacesOnlyTheBody() throws {
|
|
var document = try FrontmatterDocument.parse(Fixture.rich)
|
|
document.body = "Brand new body.\n"
|
|
let output = document.serialized()
|
|
#expect(output.hasSuffix("---\nBrand new body.\n"))
|
|
#expect(output.hasPrefix("---\n# board settings, hand-written\nschema: 1\n"))
|
|
#expect(!output.contains("More body"))
|
|
}
|
|
|
|
@Test func editsSurviveReparsing() throws {
|
|
var document = try FrontmatterDocument.parse(Fixture.rich)
|
|
document.set("order", to: .double(2048))
|
|
document.remove("modified-by")
|
|
document.set("title", to: .string("Renamed"))
|
|
|
|
let reparsed = try FrontmatterDocument.parse(document.serialized())
|
|
#expect(reparsed.order == .valid(2048))
|
|
#expect(reparsed.modifiedBy == .missing)
|
|
#expect(reparsed.title == .valid("Renamed"))
|
|
#expect(reparsed.unknownFields.map(\.key)
|
|
== ["project", "sphere", "labels", "template", "notes", "folded", "tagged", "quoted key"])
|
|
#expect(reparsed.serialized() == document.serialized())
|
|
}
|
|
}
|
|
|
|
// MARK: - Inline comments on rewritten lines
|
|
|
|
/// 01-storage-format.md § Frontmatter, "App rewrites preserve comments": an inline comment on a
|
|
/// rewritten value line is re-spliced after the new value — best-effort, guaranteed for plain
|
|
/// single-line scalars, allowed to drop in pathological shapes.
|
|
struct FrontmatterInlineCommentTests {
|
|
private func edited(_ frontmatter: String, _ key: String, _ value: FrontmatterValue) throws -> String {
|
|
var document = try FrontmatterDocument.parse("---\n\(frontmatter)\n---\nbody\n")
|
|
document.set(key, to: value)
|
|
return String(document.serialized().dropFirst("---\n".count).dropLast("\n---\nbody\n".count))
|
|
}
|
|
|
|
@Test func aPlainScalarKeepsItsCommentAndItsSpacing() throws {
|
|
#expect(try edited("order: 3072 # keep at top", "order", .double(4096))
|
|
== "order: 4096 # keep at top")
|
|
#expect(try edited("order: 3072 # tight", "order", .double(4096)) == "order: 4096 # tight")
|
|
#expect(try edited("order: 3072\t# tabbed", "order", .double(4096)) == "order: 4096\t# tabbed")
|
|
}
|
|
|
|
@Test func aValueLineWithoutACommentIsRewrittenAsBefore() throws {
|
|
#expect(try edited("order: 3072", "order", .double(4096)) == "order: 4096")
|
|
#expect(try edited("title: My Board", "title", .string("Renamed")) == "title: Renamed")
|
|
}
|
|
|
|
/// A `#` inside a quoted scalar is value text, not a comment — it must not be re-spliced.
|
|
@Test func aHashInsideAQuotedScalarIsNotAComment() throws {
|
|
#expect(try edited("title: \"hash # inside\"", "title", .string("Renamed")) == "title: Renamed")
|
|
#expect(try edited("title: 'hash # inside'", "title", .string("Renamed")) == "title: Renamed")
|
|
#expect(try edited("background: \"#ff8800\"", "background", .string("slate")) == "background: slate")
|
|
}
|
|
|
|
/// …but a real comment *after* such a value still is one.
|
|
@Test func aCommentAfterAQuotedScalarIsStillSpliced() throws {
|
|
#expect(try edited("title: \"hash # inside\" # real note", "title", .string("Renamed"))
|
|
== "title: Renamed # real note")
|
|
#expect(try edited("title: 'it''s # fine' # real note", "title", .string("Renamed"))
|
|
== "title: Renamed # real note")
|
|
}
|
|
|
|
/// An apostrophe mid-scalar does not open a quoted scalar, so the comment after it is found.
|
|
@Test func aPlainScalarWithAnApostropheKeepsItsComment() throws {
|
|
#expect(try edited("title: it's fine # note", "title", .string("Renamed"))
|
|
== "title: Renamed # note")
|
|
#expect(try edited("title: say \"hi\" # note", "title", .string("Renamed"))
|
|
== "title: Renamed # note")
|
|
}
|
|
|
|
@Test func aSingleLineFlowCollectionKeepsItsComment() throws {
|
|
#expect(try edited("labels: [a, \"b # c\"] # note", "labels", .raw("[x]"))
|
|
== "labels: [x] # note")
|
|
#expect(try edited("template: {order: 3} # picker slot", "template", .raw("{order: 7}"))
|
|
== "template: {order: 7} # picker slot")
|
|
}
|
|
|
|
@Test func aCommentOnAnEmptyValueIsKept() throws {
|
|
#expect(try edited("title: # to be filled in", "title", .string("Named"))
|
|
== "title: Named # to be filled in")
|
|
}
|
|
|
|
@Test func theCommentSurvivesSuccessiveSets() throws {
|
|
var document = try FrontmatterDocument.parse("---\norder: 3072 # keep at top\n---\nbody\n")
|
|
document.set("order", to: .double(4096))
|
|
document.set("order", to: .double(8192))
|
|
#expect(document.serialized() == "---\norder: 8192 # keep at top\n---\nbody\n")
|
|
}
|
|
|
|
/// Reading a value and writing it straight back must leave one comment, not two — the read
|
|
/// stops at the comment, the write re-splices it.
|
|
@Test func aReadThenWriteDoesNotCompoundTheComment() throws {
|
|
var document = try FrontmatterDocument.parse("---\ntitle: 2048 # note\n---\nbody\n")
|
|
#expect(document.title == .valid("2048"))
|
|
|
|
document.set("title", to: .string(document.title.value ?? ""))
|
|
#expect(document.serialized() == "---\ntitle: \"2048\" # note\n---\nbody\n")
|
|
|
|
document.set("title", to: .string(document.title.value ?? ""))
|
|
#expect(document.serialized() == "---\ntitle: \"2048\" # note\n---\nbody\n")
|
|
#expect(try FrontmatterDocument.parse(document.serialized()).title == .valid("2048"))
|
|
}
|
|
|
|
@Test func theCommentSurvivesAReparse() throws {
|
|
var document = try FrontmatterDocument.parse("---\norder: 3072 # keep at top\n---\nbody\n")
|
|
document.set("order", to: .double(4096))
|
|
let reparsed = try FrontmatterDocument.parse(document.serialized())
|
|
#expect(reparsed.order == .valid(4096))
|
|
#expect(reparsed.serialized() == document.serialized())
|
|
}
|
|
|
|
/// The winning occurrence's own comment is what carries over; a collapsed twin's inline
|
|
/// comment goes with the twin.
|
|
@Test func theWinningOccurrencesCommentIsTheOneKept() throws {
|
|
var document = try FrontmatterDocument.parse("---\norder: 1 # old\norder: 2 # current\n---\nbody\n")
|
|
document.set("order", to: .double(3))
|
|
#expect(document.serialized() == "---\norder: 3 # current\n---\nbody\n")
|
|
}
|
|
|
|
/// Pathological shapes are allowed to lose the comment rather than have the editor guess.
|
|
/// Pinning the chosen behavior: a value spanning several lines is not scanned at all.
|
|
@Test func aMultiLineValueDropsItsHeaderComment() throws {
|
|
var document = try FrontmatterDocument.parse("---\nnotes: | # about the notes\n line one\n---\nbody\n")
|
|
document.set("notes", to: .string("flattened"))
|
|
#expect(document.serialized() == "---\nnotes: flattened\n---\nbody\n")
|
|
|
|
var flow = try FrontmatterDocument.parse("---\nflow: {a: 1,\nb: 2} # spread out\nlast: x\n---\nbody\n")
|
|
flow.set("flow", to: .raw("{a: 9}"))
|
|
#expect(flow.serialized() == "---\nflow: {a: 9}\nlast: x\n---\nbody\n")
|
|
}
|
|
|
|
/// Comments on their own lines were always preserved and still are — including the one that
|
|
/// heads the frontmatter and the one that trails it.
|
|
@Test func ownLineCommentsAreUntouchedByARewrite() throws {
|
|
var document = try FrontmatterDocument.parse(Fixture.rich)
|
|
document.set("title", to: .string("Renamed"))
|
|
let output = document.serialized()
|
|
#expect(output.hasPrefix("---\n# board settings, hand-written\nschema: 1\n"))
|
|
#expect(output.contains("\n# trailing note\n---\n"))
|
|
#expect(output.contains("project: lanework # agent overlay\n"))
|
|
}
|
|
}
|
|
|
|
// MARK: - Scalar emission
|
|
|
|
struct FrontmatterEmissionTests {
|
|
private func emittedTitle(_ value: FrontmatterValue) throws -> String {
|
|
var document = try FrontmatterDocument.parse("---\n---\n")
|
|
document.set("title", to: value)
|
|
return String(document.serialized().dropFirst("---\ntitle: ".count).dropLast("\n---\n".count))
|
|
}
|
|
|
|
@Test func numbersAndTimestampsAreEmittedPlain() throws {
|
|
#expect(try emittedTitle(.int(42)) == "42")
|
|
#expect(try emittedTitle(.double(1024)) == "1024")
|
|
#expect(try emittedTitle(.double(1536.5)) == "1536.5")
|
|
#expect(try emittedTitle(.bool(true)) == "true")
|
|
#expect(try emittedTitle(.date(Date(timeIntervalSince1970: 1_785_084_098))) == "2026-07-26T16:41:38Z")
|
|
}
|
|
|
|
@Test func ordinaryStringsAreEmittedPlain() throws {
|
|
#expect(try emittedTitle(.string("My Board")) == "My Board")
|
|
#expect(try emittedTitle(.string("Fix login — round 2")) == "Fix login — round 2")
|
|
#expect(try emittedTitle(.string("a/b?c")) == "a/b?c")
|
|
}
|
|
|
|
@Test(arguments: [
|
|
"",
|
|
" padded ",
|
|
"true",
|
|
"yes",
|
|
"null",
|
|
"~",
|
|
"42",
|
|
"1.5",
|
|
"2026-07-26T16:41:38Z",
|
|
"has: a colon",
|
|
"trailing colon:",
|
|
"hash # comment",
|
|
"#ff8800",
|
|
"- leading dash",
|
|
"[bracketed]",
|
|
"{braced}",
|
|
"line\nbreak",
|
|
"tab\there",
|
|
"quote\"inside",
|
|
"back\\slash",
|
|
"*anchor",
|
|
"&anchor",
|
|
"!tagged",
|
|
"%directive",
|
|
"@at",
|
|
"|pipe",
|
|
">gt",
|
|
"'single'",
|
|
])
|
|
func stringsNeedingQuotingSurviveAReparse(value: String) throws {
|
|
var document = try FrontmatterDocument.parse("---\n---\n")
|
|
document.set("title", to: .string(value))
|
|
let reparsed = try FrontmatterDocument.parse(document.serialized())
|
|
#expect(reparsed.title == .valid(value))
|
|
}
|
|
|
|
@Test func quotingIsUsedOnlyWhenNeeded() throws {
|
|
#expect(try emittedTitle(.string("true")) == "\"true\"")
|
|
#expect(try emittedTitle(.string("#ff8800")) == "\"#ff8800\"")
|
|
#expect(try emittedTitle(.string("")) == "\"\"")
|
|
#expect(try emittedTitle(.string("a\nb")) == "\"a\\nb\"")
|
|
// An interior quote is legal in a plain scalar; a leading one is not.
|
|
#expect(try emittedTitle(.string("say \"hi\"")) == "say \"hi\"")
|
|
#expect(try emittedTitle(.string("\"quoted\"")) == "\"\\\"quoted\\\"\"")
|
|
#expect(try emittedTitle(.string("back\\slash")) == "back\\slash")
|
|
}
|
|
|
|
@Test func rawValuesAreWrittenVerbatim() throws {
|
|
var document = try FrontmatterDocument.parse("---\nschema: 1\n---\nbody\n")
|
|
document.set("template", to: .raw("{order: 7}"))
|
|
#expect(document.serialized() == "---\nschema: 1\ntemplate: {order: 7}\n---\nbody\n")
|
|
#expect(document.value(for: "template")
|
|
== .mapping([YAMLValue.Pair(key: .string("order"), value: .int(7))]))
|
|
}
|
|
}
|