1import { assert, describe, it } from "@effect/vitest"
2import * as Yaml from "effect/encoding/Yaml"
3
4describe("Yaml", () => {
5 it("parses nested block and flow collections", () => {
6 assert.deepStrictEqual(
7 Yaml.parse(`
8name: effect
9enabled: true
10ports: [3000, 3001]
11database:
12 host: localhost
13 credentials:
14 - user: root
15 roles: [admin, writer]
16 - user: guest
17 roles: []
18`),
19 {
20 name: "effect",
21 enabled: true,
22 ports: [3000, 3001],
23 database: {
24 host: "localhost",
25 credentials: [
26 { user: "root", roles: ["admin", "writer"] },
27 { user: "guest", roles: [] }
28 ]
29 }
30 }
31 )
32 })
33
34 it("parses quoted and block scalars", () => {
35 assert.deepStrictEqual(
36 Yaml.parse(`
37quoted: "line\\nvalue"
38literal: |
39 first
40 second
41folded: >-
42 first
43 second
44`),
45 {
46 quoted: "line\nvalue",
47 literal: "first\nsecond\n",
48 folded: "first second"
49 }
50 )
51 })
52
53 it("preserves paragraph and indentation breaks in folded block scalars", () => {
54 assert.deepStrictEqual(
55 Yaml.parse(`
56paragraph: >-
57 first
58
59 second
60indented: >-
61 first
62 second
63 third
64`),
65 {
66 paragraph: "first\nsecond",
67 indented: "first\n second\nthird"
68 }
69 )
70 })
71
72 it.each([
73 [
74 "leading blanks before a more-indented first line",
75 "message: >-2\n\n first\n second\n",
76 "\n first\nsecond"
77 ],
78 [
79 "keep chomping after a more-indented final line",
80 "message: >+2\n first\n second\n",
81 "first\n second\n"
82 ],
83 [
84 "multiple blank paragraph lines",
85 "message: >-\n first\n\n\n second\n",
86 "first\n\nsecond"
87 ]
88 ])("preserves %s", (_, source, expected) => {
89 assert.deepStrictEqual(Yaml.parse(source), { message: expected })
90 })
91
92 it("resolves aliases", () => {
93 assert.deepStrictEqual(
94 Yaml.parse(`
95defaults: &defaults
96 host: localhost
97 port: 5432
98development:
99 settings: *defaults
100`),
101 {
102 defaults: { host: "localhost", port: 5432 },
103 development: { settings: { host: "localhost", port: 5432 } }
104 }
105 )
106 })
107
108 it("rejects invalid indentation", () => {
109 assert.throws(() => Yaml.parse("root:\n child: true\n sibling: false\n"))
110 })
111
112 // YAML 1.2.2 §8.2.1: a mapping value's block sequence may be indentless.
113 describe("indentless sequences", () => {
114 it.each([
115 {
116 name: "top-level mapping value",
117 source: "allowed-tools:\n- Read\n- Bash\n",
118 expected: { "allowed-tools": ["Read", "Bash"] }
119 },
120 {
121 name: "nested mapping value",
122 source: "metadata:\n tools:\n - Read\n",
123 expected: { metadata: { tools: ["Read"] } }
124 },
125 {
126 name: "multiple sequences and sibling keys",
127 source: "tools:\n- Read\n- Bash\nroles:\n- admin\nenabled: true\n",
128 expected: { tools: ["Read", "Bash"], roles: ["admin"], enabled: true }
129 },
130 {
131 name: "comments and blank lines before and between items",
132 source: "tools: # key comment\n# before sequence\n\n- Read # item comment\n # between items\n\n- Bash\n",
133 expected: { tools: ["Read", "Bash"] }
134 },
135 {
136 name: "nested mapping items and their indentless sequences",
137 source: "steps:\n- name: deploy\n tools:\n - Read\n - Bash\n enabled: true\n- name: verify\n",
138 expected: { steps: [{ name: "deploy", tools: ["Read", "Bash"], enabled: true }, { name: "verify" }] }
139 },
140 {
141 name: "nested sequences ending at mapping boundaries",
142 source: "metadata:\n tools:\n - Read\n roles:\n - admin\n enabled: true\nname: deploy\n",
143 expected: { metadata: { tools: ["Read"], roles: ["admin"], enabled: true }, name: "deploy" }
144 },
145 {
146 name: "multiline descriptions alongside sequences",
147 source:
148 "description: Deploy the service\n and verify it.\ntools:\n- Read\nmetadata:\n description: 'Check\n the logs'\n tools:\n - Bash\n",
149 expected: {
150 description: "Deploy the service and verify it.",
151 tools: ["Read"],
152 metadata: { description: "Check the logs", tools: ["Bash"] }
153 }
154 }
155 ])("parses $name", ({ source, expected }) => {
156 assert.deepStrictEqual(Yaml.parse(source), expected)
157 })
158 })
159
160 // YAML 1.2.2 §§6.5–6.7 and 7.3.1–7.3.3: flow scalar folding and comments.
161 // Expected values and rejection fixtures were checked with yaml 2.9.1.
162 describe("multiline flow scalars", () => {
163 it.each([
164 [
165 "plain scalar starting below the key",
166 "description:\n Deploy the service\n and verify it.\n",
167 "Deploy the service and verify it."
168 ],
169 [
170 "plain scalar starting on the key line",
171 "description: Deploy the service\n and verify it.\n",
172 "Deploy the service and verify it."
173 ],
174 [
175 "varying continuation indentation",
176 "description:\n Deploy\n the service\n and verify it.\n",
177 "Deploy the service and verify it."
178 ],
179 ["one blank line", "description: Deploy\n\n and verify.\n", "Deploy\nand verify."],
180 ["multiple blank lines", "description: Deploy\n\n\n and verify.\n", "Deploy\n\nand verify."],
181 ["whitespace-only blank line", "description: Deploy\n \n and verify.\n", "Deploy\nand verify."],
182 [
183 "leading and trailing comments",
184 "# before key\ndescription: # before value\n # before scalar\n Deploy\n and verify. # after scalar\n# after value\n",
185 "Deploy and verify."
186 ],
187 ["trailing whitespace", "description: Deploy \n and verify. \n", "Deploy and verify."],
188 ["CRLF line breaks", "description: Deploy\r\n and verify.\r\n", "Deploy and verify."],
189 ["double-quoted scalar", "description: \"Deploy it\n and more\"\n", "Deploy it and more"],
190 ["single-quoted scalar", "description: 'Deploy it\n and more'\n", "Deploy it and more"],
191 ["single-quoted apostrophe", "description: 'Deploy it\n and don''t stop'\n", "Deploy it and don't stop"],
192 ["double-quoted blank lines", "description: \"Deploy\n\n\n and verify.\"\n", "Deploy\n\nand verify."],
193 ["single-quoted blank line", "description: 'Deploy\n\n and verify.'\n", "Deploy\nand verify."],
194 ["quoted whitespace folding", "description: \"Deploy \n and verify.\"\n", "Deploy and verify."],
195 ["double-quoted escaped line break", "description: \"De\\\n ploy\"\n", "Deploy"],
196 ["space before escaped line break", "description: \"Deploy \\\n it\"\n", "Deploy it"],
197 ["escaped line break followed by blank line", "description: \"Deploy\\\n\n it\"\n", "Deploy it"],
198 ["space before escaped line break followed by blank line", "description: \"Deploy \\\n\n it\"\n", "Deploy it"],
199 ["escaped leading space after line break", "description: \"Deploy\\\n \\ it\"\n", "Deploy it"],
200 ["escaped trailing space before a folded break", "description: \"Deploy\\ \n it\"\n", "Deploy it"],
201 ["escaped break followed by multiple blank lines", "description: \"Deploy\\\n\n\n it\"\n", "Deploy\nit"],
202 ["hash inside double quotes", "description: \"Deploy\n # still text\" # comment\n", "Deploy # still text"],
203 ["hash inside single quotes", "description: 'Deploy\n # still text' # comment\n", "Deploy # still text"],
204 [
205 "colon and hash without separating spaces",
206 "description: Visit https://example.com\n for issue#123\n",
207 "Visit https://example.com for issue#123"
208 ],
209 ["apostrophe in plain text before a comment", "description: Don't deploy # comment\n", "Don't deploy"],
210 ["quote in plain text before a comment", "description: Use a \"quote # comment\n", "Use a \"quote"],
211 ["single quote starting a plain continuation", "description: Deploy\n 'it # end\n", "Deploy 'it"],
212 ["double quote starting a plain continuation", "description: Deploy\n \"it # end\n", "Deploy \"it"],
213 ["bracket and quote inside plain text", "description: plain [ a 'quote # comment\n", "plain [ a 'quote"]
214 ])("folds %s", (_, source, expected) => {
215 assert.deepStrictEqual(Yaml.parse(source), { description: expected })
216 const nested = source.split(/\r?\n/).map((line) => ` ${line}`).join("\n")
217 assert.deepStrictEqual(Yaml.parse(`metadata:\n${nested}`), { metadata: { description: expected } })
218 })
219
220 it("ends plain scalars at sibling keys after terminating comments", () => {
221 assert.deepStrictEqual(
222 Yaml.parse("description: Deploy\n and verify. # end\n# between keys\nenabled: true\n"),
223 { description: "Deploy and verify.", enabled: true }
224 )
225 })
226
227 it("folds plain sequence items", () => {
228 assert.deepStrictEqual(Yaml.parse("- Deploy\n and verify.\n- Finish\n"), ["Deploy and verify.", "Finish"])
229 })
230 })
231
232 describe("scalar conformance boundaries", () => {
233 it.each([
234 ["colon followed by space", "description: Use when: deploy\n"],
235 ["colon at end of plain scalar", "description: Deploy:\n"],
236 ["colon followed by tab", "description: Use when:\tdeploy\n"],
237 ["colon in a continuation", "description: Deploy\n when: ready\n"],
238 ["continuation after an inline comment", "description: Deploy # end\n and verify.\n"],
239 ["continuation after a comment line", "description: Deploy\n # end\n and verify.\n"],
240 ["continuation after a trailing continuation comment", "description: Deploy\n and verify # end\n again\n"],
241 ["continuation after a comment following a literal single quote", "description: Deploy\n 'it # end\n again\n"],
242 ["continuation after a comment following a literal double quote", "description: Deploy\n \"it # end\n again\n"],
243 ["unterminated double quote", "description: \"Deploy\n and verify\n"],
244 ["unterminated single quote", "description: 'Deploy\n and verify\n"],
245 ["content after closing double quote", "description: \"Deploy\n it\" extra\n"],
246 ["content after closing single quote", "description: 'Deploy\n it' extra\n"],
247 ["tab-indented scalar", "description:\n\tDeploy\n"],
248 ["duplicate key", "description: first\ndescription: second\n"]
249 ])("rejects %s", (_, source) => {
250 assert.throws(() => Yaml.parse(source), SyntaxError)
251 })
252
253 it("preserves numeric decoding and quoted colon content", () => {
254 assert.deepStrictEqual(
255 Yaml.parse("description: 123\nquoted: 'Use when: deploy'\ntrailing: \"Deploy:\"\n"),
256 { description: 123, quoted: "Use when: deploy", trailing: "Deploy:" }
257 )
258 })
259 })
260
261 describe("malformed content around compatibility shapes", () => {
262 const shapes = [
263 ["indentless sequence", "tools:\n- Read\n- Bash\n"],
264 ["nested indentless sequence", "metadata:\n tools:\n - Read\n"],
265 ["multiline plain scalar", "description: Deploy\n and verify.\n"],
266 ["multiline quoted scalar", "description: \"Deploy\n and verify.\"\n"]
267 ]
268 it.each([
269 ["unterminated flow sequence", "broken: [one, two\n"],
270 ["unterminated flow mapping", "broken: {one: two\n"],
271 ["unmatched top-level line", "broken: true\nunmatched\n"],
272 ["tab indentation", "broken:\n\tchild: true\n"],
273 ["invalid mapping indentation", "broken:\n child: true\n sibling: false\n"],
274 ["duplicate keys", "broken: one\nbroken: two\n"]
275 ])("rejects %s alone and before or after valid shapes", (_, malformed) => {
276 assert.throws(() => Yaml.parse(malformed), SyntaxError)
277 for (const [name, source] of shapes) {
278 assert.throws(() => Yaml.parse(malformed + source), SyntaxError, undefined, `before ${name}`)
279 assert.throws(() => Yaml.parse(source + malformed), SyntaxError, undefined, `after ${name}`)
280 }
281 })
282 })
283
284 describe("unsupported compact nested block sequences", () => {
285 // Valid YAML, deliberately rejected as unsupported.
286 it.each([
287 ["indentless mapping value", "a:\n- - 1\n - 2\n"],
288 ["indented mapping value", "a:\n - - 1\n - 2\n"],
289 ["root sequence", "- - 1\n - 2\n"],
290 ["blank line between nested entries", "- - 1\n\n - 2\n"],
291 ["nested entry after a scalar item", "a:\n- 1\n- - 2\n"],
292 ["nested mapping item", "a:\n- - b: 1\n"],
293 ["single-line nested sequence", "- - 1\n"],
294 ["empty nested entry", "- -\n"]
295 ])("rejects %s", (_, source) => {
296 assert.throws(() => Yaml.parse(source), SyntaxError)
297 })
298
299 it.each(
300 [
301 ["mapping value", "a: one\n - two\n", { a: "one - two" }],
302 ["sequence item", "- one\n - two\n", ["one - two"]]
303 ] as const
304 )("preserves a dash in a plain continuation of a %s", (_, source, expected) => {
305 assert.deepStrictEqual(Yaml.parse(source), expected)
306 })
307 })
308
309 describe("document boundaries", () => {
310 // YAML 1.2.2 §9.1.2: document markers must start at column zero.
311 it.each([
312 ["---", ""],
313 ["---", " # comment"],
314 ["...", ""],
315 ["...", " # comment"]
316 ])("preserves indented %s%s as a mapping value", (marker, suffix) => {
317 assert.deepStrictEqual(Yaml.parse(`a:\n ${marker}${suffix}\n`), { a: marker })
318 assert.deepStrictEqual(Yaml.parse(`metadata:\n a:\n ${marker}${suffix}\n`), { metadata: { a: marker } })
319 })
320
321 it.each([
322 ["mappings", "a: 1\n---\nb: 2\n"],
323 ["sequences", "- one\n---\n- two\n"],
324 ["plain scalars", "one\n---\ntwo\n"],
325 ["explicit initial marker", "---\na: 1\n--- # second document\nb: 2\n"],
326 ["empty first document", "---\n---\nb: 2\n"],
327 ["empty trailing document", "a: 1\n---\n"]
328 ])("rejects a document stream containing %s", (_, source) => {
329 assert.throws(() => Yaml.parse(source), SyntaxError)
330 })
331
332 it.each(
333 [
334 ["mapping", "# before\n--- # start\na: 1\n... # end\n# after\n", { a: 1 }],
335 ["sequence", "---\n- one\n- two\n...\n", ["one", "two"]],
336 ["plain scalar", "---\none\n...\n", "one"],
337 ["empty document", "---\n...\n", null]
338 ] as const
339 )("preserves single-document markers around a %s", (_, source, expected) => {
340 assert.deepStrictEqual(Yaml.parse(source), expected)
341 })
342 })
343
344 describe("anchors and quoted comment boundaries", () => {
345 it.each([
346 ["plain", "description: &text Deploy\n and verify.\nalias: *text\n", "Deploy and verify."],
347 [
348 "single-quoted",
349 "description: &text 'Deploy # first\n # second' # end\nalias: *text\n",
350 "Deploy # first # second"
351 ],
352 [
353 "double-quoted",
354 "description: &text \"Deploy # first\n # second\" # end\nalias: *text\n",
355 "Deploy # first # second"
356 ]
357 ])("resolves anchored multiline %s scalars", (_, source, expected) => {
358 assert.deepStrictEqual(Yaml.parse(source), { description: expected, alias: expected })
359 })
360
361 it("resolves anchored nested indentless sequences", () => {
362 assert.deepStrictEqual(
363 Yaml.parse("metadata:\n tools: &tools\n - Read\n - Bash\n alias: *tools\n"),
364 { metadata: { tools: ["Read", "Bash"], alias: ["Read", "Bash"] } }
365 )
366 })
367
368 it("preserves hashes in JSON-style flow strings without colon spacing", () => {
369 assert.deepStrictEqual(Yaml.parse("{\"description\":\"Deploy # now\"} # end\n"), { description: "Deploy # now" })
370 })
371
372 it("preserves hashes in single- and double-quoted flow sequence items", () => {
373 assert.deepStrictEqual(
374 Yaml.parse("descriptions: [\"Deploy # now\", 'Verify # later'] # end\n"),
375 { descriptions: ["Deploy # now", "Verify # later"] }
376 )
377 })
378 })
379})