char/fjson

friendly JSON implementation in TypeScript

git clone https://git.t4t.associates/char/fjson

Charlotte Somrestructurebb03820

main
9.3 KiB290 linesraw
1// vibed
2
3import {
4  deepStrictEqual,
5  equal,
6  ok,
7  strictEqual,
8  throws,
9} from "node:assert/strict";
10import { Document, FjsonError, parse } from "./mod.ts";
11
12Deno.test("replacing a leaf preserves every other byte and quote style", () => {
13  const source =
14    "// head\r\nimports: { 'de\\u0070': /* choice */ 'old' /* reason */ },\r\nother: 1e3\r\n";
15  const document = Document.parse(source);
16  document.set(["imports", "dep"], "it's new\nšŸ˜€");
17  strictEqual(
18    document.source,
19    source.replace("'old'", "'it\\'s new\\nšŸ˜€'"),
20  );
21  deepStrictEqual(parse(document.source), {
22    imports: { dep: "it's new\nšŸ˜€" },
23    other: 1000,
24  });
25});
26
27Deno.test("unchanged values do not reformat or remove internal comments", () => {
28  const source = "items: [1, /* why */ 2,]\nname: 'same'\n";
29  const document = Document.parse(source);
30  document.set(["items"], [1, 2]);
31  document.set(["name"], "same");
32  strictEqual(document.source, source);
33});
34
35Deno.test("insertion preserves comments, line endings, and existing layout", () => {
36  for (const newline of ["\n", "\r\n", "\r"]) {
37    const source =
38      `// head${newline}options: {${newline}\told: 'kept' // reason${newline}}${newline}`;
39    const document = Document.parse(source);
40    document.set(["options", "new-key"], { enabled: true });
41    ok(document.source.includes(`\told: 'kept' // reason${newline}`));
42    ok(
43      document.source.includes(
44        `\t"new-key": {${newline}\t  enabled: true${newline}\t}`,
45      ),
46    );
47    deepStrictEqual(parse(document.source), {
48      options: { old: "kept", "new-key": { enabled: true } },
49    });
50    if (newline !== "\n") {
51      ok(!document.source.replaceAll(newline, "").includes("\n"));
52    }
53  }
54  for (
55    const source of [
56      "",
57      "// eof",
58      "{/* empty */}",
59      "/* heading */\n",
60      "{\n  // empty\n}",
61    ]
62  ) {
63    const document = Document.parse(source);
64    document.set(["ę—„ęœ¬čŖž"], [true, false]);
65    deepStrictEqual(parse(document.source), { "ę—„ęœ¬čŖž": [true, false] });
66    for (
67      const comment of ["// eof", "/* empty */", "/* heading */", "// empty"]
68    ) {
69      if (source.includes(comment)) {
70        ok(document.source.includes(comment), comment);
71      }
72    }
73  }
74});
75
76Deno.test("array appends and sequential edits use updated paths", () => {
77  const document = Document.parse(
78    "list: [/* first */ 1, // note\n 2,]\nother: 'keep'",
79  );
80  document.set(["list", 2], { name: "new" });
81  document.remove(["list", 0]);
82  document.set(["list", 1, "name"], "updated");
83  deepStrictEqual(parse(document.source), {
84    list: [2, { name: "updated" }],
85    other: "keep",
86  });
87  for (const kept of ["/* first */", "// note", "other: 'keep'"]) {
88    ok(document.source.includes(kept), kept);
89  }
90});
91
92Deno.test("removing a member keeps external comments but deletes its contents", () => {
93  const source =
94    "// head\na /* key : šŸ˜€ */ : /* before */ { nested: [/* inside */ 1] } /* after , */ , // line\nb: 'kept'";
95  const document = Document.parse(source);
96  strictEqual(document.remove(["a"]), true);
97  deepStrictEqual(parse(document.source), { b: "kept" });
98  for (
99    const comment of [
100      "// head",
101      "/* key : šŸ˜€ */",
102      "/* before */",
103      "/* after , */",
104      "// line",
105    ]
106  ) {
107    ok(document.source.includes(comment), comment);
108  }
109  ok(!document.source.includes("/* inside */"));
110  ok(document.source.endsWith("b: 'kept'"));
111  strictEqual(document.remove(["a"]), false);
112});
113
114Deno.test("removal handles all separator styles and positions", () => {
115  for (
116    const separator of [
117      ",",
118      ", ",
119      "\n",
120      " /* gap , šŸ˜€ */\n",
121      ", // gap\n",
122      "\r\n",
123    ]
124  ) {
125    for (const trailing of ["", ",", "\n", ",\n"]) {
126      for (const braced of [false, true]) {
127        for (let index = 0; index < 3; index += 1) {
128          const body = `a: 1${separator}b: 2${separator}c: 3${trailing}`;
129          const document = Document.parse(braced ? `{${body}}` : body);
130          const keys = ["a", "b", "c"];
131          strictEqual(document.remove([keys[index]]), true);
132          const expected: Record<string, number> = { a: 1, b: 2, c: 3 };
133          delete expected[keys[index]];
134          deepStrictEqual(parse(document.source), expected, document.source);
135          for (const key of keys) document.remove([key]);
136          deepStrictEqual(parse(document.source), {});
137          document.set(["new"], true);
138          deepStrictEqual(parse(document.source), { new: true });
139        }
140      }
141      for (let index = 0; index < 3; index += 1) {
142        const document = Document.parse(
143          `[1${separator}2${separator}3${trailing}]`,
144        );
145        document.remove([index]);
146        const expected = [1, 2, 3];
147        expected.splice(index, 1);
148        deepStrictEqual(parse(document.source), expected);
149        document.remove([0]);
150        document.remove([0]);
151        deepStrictEqual(parse(document.source), []);
152        document.set([0], "again");
153        deepStrictEqual(parse(document.source), ["again"]);
154      }
155    }
156  }
157});
158
159Deno.test("replacing a container removes only comments inside it", () => {
160  const document = Document.parse(
161    "keep: 1\nobject: /* before */ {x: 1 /* inside */} // after\n",
162  );
163  document.set(["object"], false);
164  strictEqual(
165    document.source,
166    "keep: 1\nobject: /* before */ false // after\n",
167  );
168  const root = Document.parse("// head\n[/* inside */ 1] // tail");
169  root.set([], { new: true });
170  strictEqual(root.source, "// head\n{\n  new: true\n} // tail");
171});
172
173Deno.test("root replacements preserve surrounding trivia", () => {
174  for (
175    const source of [
176      "",
177      "  ",
178      "// heading",
179      "  // heading",
180      "/* heading */",
181      "// heading\n",
182      "// heading\n{} // tail",
183      "// heading\n[1] // tail",
184      "// heading\nfalse // tail",
185      "// heading\nold: true",
186      "// heading\nold: true // tail\n",
187      "// heading\nold: true,\n// tail\n",
188    ]
189  ) {
190    for (const value of [true, null, 42, "new", { new: true }, [], [1, 2]]) {
191      const document = Document.parse(source);
192      document.set([], value);
193      deepStrictEqual(parse(document.source), value, document.source);
194      for (const kept of ["// heading", "/* heading */", "// tail"]) {
195        if (source.includes(kept)) ok(document.source.includes(kept), kept);
196      }
197    }
198  }
199});
200
201Deno.test("removing the last implicit member preserves external comments", () => {
202  const source =
203    "a /* key */ : /* before */ { x: 1 /* inside */ } /* after, */ , // tail\n";
204  const document = Document.parse(source);
205  document.remove(["a"]);
206  deepStrictEqual(parse(document.source), {});
207  for (
208    const comment of ["/* key */", "/* before */", "/* after, */", "// tail"]
209  ) {
210    ok(document.source.includes(comment), comment);
211  }
212  ok(!document.source.includes("/* inside */"));
213  document.set(["b"], 2);
214  deepStrictEqual(parse(document.source), { b: 2 });
215});
216
217Deno.test("object documents require object roots", () => {
218  const error = (() => {
219    try {
220      Document.parseObject("imports {}");
221    } catch (caught) {
222      ok(caught instanceof FjsonError);
223      return caught;
224    }
225    throw new Error("expected error");
226  })();
227  strictEqual(error.render("config", "imports {}"), "config:1:9: Expected `:`");
228  for (const source of ["false", "1", "[]", "'a'"]) {
229    throws(() => Document.parseObject(source), FjsonError, source);
230  }
231  const document = Document.parseObject("a: 1");
232  document.set([], 5);
233  deepStrictEqual(document.toValue(), 5);
234});
235
236Deno.test("invalid paths and over-limit edits are atomic", () => {
237  const source = "items: [1]\nscalar: false";
238  const document = Document.parse(source);
239  const bad: (string | number)[][] = [
240    ["missing", "child"],
241    ["items", 2],
242    ["items", "wrong"],
243    ["scalar", "child"],
244    [0],
245  ];
246  for (const path of bad) {
247    throws(() => document.set(path, true), FjsonError, JSON.stringify(path));
248    strictEqual(document.source, source);
249  }
250  throws(() => document.remove([]), FjsonError);
251  strictEqual(document.source, source);
252  let nested: unknown = null;
253  for (let i = 0; i < 130; i += 1) nested = [nested];
254  const error = (() => {
255    try {
256      document.set(["items"], nested);
257    } catch (caught) {
258      ok(caught instanceof FjsonError);
259      return caught;
260    }
261    throw new Error("expected nesting error");
262  })();
263  strictEqual(error.kind, "nesting");
264  error.render("test", document.source);
265  strictEqual(document.source, source);
266});
267
268Deno.test("documents remember their options for later edits", () => {
269  const document = Document.parse("value: [[[0]]]", { maxDepth: 10 });
270  deepStrictEqual(document.options, { maxDepth: 10 });
271  let deep: unknown = null;
272  for (let i = 0; i < 10; i += 1) deep = [deep];
273  document.set(["value"], deep);
274  deepStrictEqual(document.toValue(), { value: deep });
275  throws(() => Document.parse("value: [[[0]]]", { maxDepth: 2 }), FjsonError);
276});
277
278Deno.test("unserializable values throw TypeError", () => {
279  const document = Document.parse("a: 1");
280  for (const value of [undefined, () => {}, Symbol("x"), 10n]) {
281    throws(() => document.set(["a"], value), TypeError);
282  }
283  strictEqual(document.source, "a: 1");
284});
285
286Deno.test("toValue converts the document", () => {
287  const document = Document.parse("a: [1, {b: 'x'}]");
288  deepStrictEqual(document.toValue(), { a: [1, { b: "x" }] });
289  strictEqual(document.root().kind, "object");
290});