jsonc-effect 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ast.js +255 -0
- package/equality.js +166 -0
- package/errors.js +218 -0
- package/format.js +453 -0
- package/index.d.ts +1933 -1983
- package/index.js +11 -1454
- package/package.json +50 -56
- package/parse.js +548 -0
- package/scanner.js +350 -0
- package/schema-integration.js +178 -0
- package/schemas.js +325 -0
- package/tsdoc-metadata.json +11 -11
- package/visitor.js +355 -0
package/ast.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { Effect, Function, Option } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/ast.ts
|
|
4
|
+
/**
|
|
5
|
+
* AST navigation functions for JSONC parse trees.
|
|
6
|
+
*
|
|
7
|
+
* Operates on JsoncNode trees produced by parseTree().
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Find an AST node at a specific JSON path.
|
|
13
|
+
*
|
|
14
|
+
* Supports {@link https://effect.website/docs/effect/function#dual | Function.dual}
|
|
15
|
+
* for both data-first and data-last (pipeline) usage.
|
|
16
|
+
*
|
|
17
|
+
* @param root - The AST root node obtained from {@link parseTree}.
|
|
18
|
+
* @param path - An array of string keys and numeric indices describing the path to traverse.
|
|
19
|
+
*
|
|
20
|
+
* @returns `Effect<Option<JsoncNode>>` — the node at the given path, or `Option.none()` if the
|
|
21
|
+
* path does not exist in the tree.
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* String segments navigate object properties and number segments navigate array indices.
|
|
25
|
+
* Returns `Option.none()` when any segment along the path cannot be resolved — for example,
|
|
26
|
+
* accessing a property on a non-object node or an out-of-bounds array index.
|
|
27
|
+
*
|
|
28
|
+
* @see {@link parseTree} — produces the AST root this function operates on
|
|
29
|
+
* @see {@link getNodeValue} — reconstructs a JS value from a found node
|
|
30
|
+
* @see {@link JsoncNode} — the AST node type
|
|
31
|
+
* @see {@link (JsoncPath:type)} — the path segment array type
|
|
32
|
+
*
|
|
33
|
+
* @example Data-first usage
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { Effect, Option } from "effect";
|
|
36
|
+
* import type { JsoncNode } from "jsonc-effect";
|
|
37
|
+
* import { parseTree, findNode } from "jsonc-effect";
|
|
38
|
+
*
|
|
39
|
+
* const program = Effect.gen(function* () {
|
|
40
|
+
* const root: Option.Option<JsoncNode> = yield* parseTree('{ "a": { "b": 1 } }');
|
|
41
|
+
* if (Option.isNone(root)) return Option.none();
|
|
42
|
+
* return yield* findNode(root.value, ["a", "b"]);
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* const result = Effect.runSync(program);
|
|
46
|
+
* // result is Option.some(node) where node.value === 1
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @example Data-last pipeline usage
|
|
50
|
+
* ```ts
|
|
51
|
+
* import { Effect, Option, pipe } from "effect";
|
|
52
|
+
* import type { JsoncNode } from "jsonc-effect";
|
|
53
|
+
* import { parseTree, findNode } from "jsonc-effect";
|
|
54
|
+
*
|
|
55
|
+
* const program = Effect.gen(function* () {
|
|
56
|
+
* const root: Option.Option<JsoncNode> = yield* parseTree('{ "x": [10, 20] }');
|
|
57
|
+
* if (Option.isNone(root)) return Option.none();
|
|
58
|
+
* return yield* pipe(root.value, findNode(["x", 1]));
|
|
59
|
+
* });
|
|
60
|
+
*
|
|
61
|
+
* const result = Effect.runSync(program);
|
|
62
|
+
* // result is Option.some(node) where node.value === 20
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @privateRemarks
|
|
66
|
+
* Wrapped in `Effect.sync`; the underlying traversal is fully synchronous.
|
|
67
|
+
*
|
|
68
|
+
* @public
|
|
69
|
+
*/
|
|
70
|
+
const findNode = Function.dual(2, (root, path) => Effect.sync(() => findNodeImpl(root, path)));
|
|
71
|
+
/**
|
|
72
|
+
* Find the innermost AST node covering a character offset.
|
|
73
|
+
*
|
|
74
|
+
* Supports {@link https://effect.website/docs/effect/function#dual | Function.dual}
|
|
75
|
+
* for both data-first and data-last (pipeline) usage.
|
|
76
|
+
*
|
|
77
|
+
* @param root - The AST root node obtained from {@link parseTree}.
|
|
78
|
+
* @param offset - A zero-based character offset into the original JSONC string.
|
|
79
|
+
*
|
|
80
|
+
* @returns `Effect<Option<JsoncNode>>` — the most deeply nested node whose span
|
|
81
|
+
* includes the offset, or `Option.none()` if the offset is outside the tree.
|
|
82
|
+
*
|
|
83
|
+
* @remarks
|
|
84
|
+
* This is useful for editor integrations such as hover information, go-to-definition,
|
|
85
|
+
* and code completions where you need to identify the token under the cursor.
|
|
86
|
+
*
|
|
87
|
+
* @see {@link parseTree} — produces the AST root this function operates on
|
|
88
|
+
* @see {@link getNodePath} — returns the JSON path to the node at an offset
|
|
89
|
+
*
|
|
90
|
+
* @example Finding a node at an offset
|
|
91
|
+
* ```ts
|
|
92
|
+
* import { Effect, Option } from "effect";
|
|
93
|
+
* import type { JsoncNode } from "jsonc-effect";
|
|
94
|
+
* import { parseTree, findNodeAtOffset } from "jsonc-effect";
|
|
95
|
+
*
|
|
96
|
+
* const program = Effect.gen(function* () {
|
|
97
|
+
* const root: Option.Option<JsoncNode> = yield* parseTree('{ "key": "value" }');
|
|
98
|
+
* if (Option.isNone(root)) return Option.none();
|
|
99
|
+
* // Offset 10 is inside the "value" string literal
|
|
100
|
+
* return yield* findNodeAtOffset(root.value, 10);
|
|
101
|
+
* });
|
|
102
|
+
*
|
|
103
|
+
* const result = Effect.runSync(program);
|
|
104
|
+
* // result is Option.some(node) where node.type === "string"
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* @privateRemarks
|
|
108
|
+
* Wrapped in `Effect.sync`; the underlying traversal is fully synchronous.
|
|
109
|
+
*
|
|
110
|
+
* @public
|
|
111
|
+
*/
|
|
112
|
+
const findNodeAtOffset = Function.dual(2, (root, offset) => Effect.sync(() => findNodeAtOffsetImpl(root, offset)));
|
|
113
|
+
/**
|
|
114
|
+
* Get the JSON path to the node at a specific character offset.
|
|
115
|
+
*
|
|
116
|
+
* Supports {@link https://effect.website/docs/effect/function#dual | Function.dual}
|
|
117
|
+
* for both data-first and data-last (pipeline) usage.
|
|
118
|
+
*
|
|
119
|
+
* @param root - The AST root node obtained from {@link parseTree}.
|
|
120
|
+
* @param targetOffset - A zero-based character offset into the original JSONC string.
|
|
121
|
+
*
|
|
122
|
+
* @returns `Effect<Option<JsoncPath>>` — an array of path segments (string keys
|
|
123
|
+
* and numeric indices) leading to the innermost node at the offset, or
|
|
124
|
+
* `Option.none()` if the offset is outside the tree.
|
|
125
|
+
*
|
|
126
|
+
* @remarks
|
|
127
|
+
* Returns the path segments leading to the innermost node that spans the given
|
|
128
|
+
* offset. This is the inverse of {@link findNode} — given an offset you get the path,
|
|
129
|
+
* and given a path you get the node.
|
|
130
|
+
*
|
|
131
|
+
* @see {@link findNodeAtOffset} — returns the node itself rather than its path
|
|
132
|
+
* @see {@link (JsoncPath:type)} — the path segment array type
|
|
133
|
+
*
|
|
134
|
+
* @example Getting the path at an offset
|
|
135
|
+
* ```ts
|
|
136
|
+
* import { Effect, Option } from "effect";
|
|
137
|
+
* import type { JsoncNode, JsoncPath } from "jsonc-effect";
|
|
138
|
+
* import { parseTree, getNodePath } from "jsonc-effect";
|
|
139
|
+
*
|
|
140
|
+
* const program = Effect.gen(function* () {
|
|
141
|
+
* const root: Option.Option<JsoncNode> = yield* parseTree('{ "a": { "b": 42 } }');
|
|
142
|
+
* if (Option.isNone(root)) return Option.none();
|
|
143
|
+
* // Offset 15 points inside the value 42
|
|
144
|
+
* return yield* getNodePath(root.value, 15);
|
|
145
|
+
* });
|
|
146
|
+
*
|
|
147
|
+
* const result: Option.Option<JsoncPath> = Effect.runSync(program);
|
|
148
|
+
* // result is Option.some(["a", "b"])
|
|
149
|
+
* ```
|
|
150
|
+
*
|
|
151
|
+
* @public
|
|
152
|
+
*/
|
|
153
|
+
const getNodePath = Function.dual(2, (root, targetOffset) => Effect.sync(() => buildPath(root, targetOffset, [])));
|
|
154
|
+
/**
|
|
155
|
+
* Reconstruct a plain JavaScript value from an AST subtree.
|
|
156
|
+
*
|
|
157
|
+
* @param node - The AST node to evaluate, typically obtained via {@link findNode} or
|
|
158
|
+
* as the root from {@link parseTree}.
|
|
159
|
+
*
|
|
160
|
+
* @returns `Effect<unknown>` — the reconstructed JS value (object, array, string,
|
|
161
|
+
* number, boolean, or null).
|
|
162
|
+
*
|
|
163
|
+
* @remarks
|
|
164
|
+
* Recursively evaluates the node tree to produce a plain JavaScript value.
|
|
165
|
+
* This is the inverse of {@link parseTree}: where `parseTree` turns a JSONC string
|
|
166
|
+
* into an AST, `getNodeValue` turns an AST subtree back into a JS value.
|
|
167
|
+
*
|
|
168
|
+
* @see {@link parseTree} — produces the AST that this function evaluates
|
|
169
|
+
* @see {@link findNode} — locates a subtree to pass to this function
|
|
170
|
+
*
|
|
171
|
+
* @example Extracting the value of a found node
|
|
172
|
+
* ```ts
|
|
173
|
+
* import { Effect, Option } from "effect";
|
|
174
|
+
* import type { JsoncNode } from "jsonc-effect";
|
|
175
|
+
* import { parseTree, findNode, getNodeValue } from "jsonc-effect";
|
|
176
|
+
*
|
|
177
|
+
* const program = Effect.gen(function* () {
|
|
178
|
+
* const root: Option.Option<JsoncNode> = yield* parseTree('{ "items": [1, 2, 3] }');
|
|
179
|
+
* if (Option.isNone(root)) return undefined;
|
|
180
|
+
* const node: Option.Option<JsoncNode> = yield* findNode(root.value, ["items"]);
|
|
181
|
+
* if (Option.isNone(node)) return undefined;
|
|
182
|
+
* return yield* getNodeValue(node.value);
|
|
183
|
+
* });
|
|
184
|
+
*
|
|
185
|
+
* const result = Effect.runSync(program);
|
|
186
|
+
* // result is [1, 2, 3]
|
|
187
|
+
* ```
|
|
188
|
+
*
|
|
189
|
+
* @privateRemarks
|
|
190
|
+
* Useful after {@link findNode} to extract a subtree's value without manual
|
|
191
|
+
* AST traversal.
|
|
192
|
+
*
|
|
193
|
+
* @public
|
|
194
|
+
*/
|
|
195
|
+
const getNodeValue = (node) => Effect.sync(() => evaluateNode(node));
|
|
196
|
+
function findNodeImpl(root, path) {
|
|
197
|
+
let current = root;
|
|
198
|
+
for (const segment of path) {
|
|
199
|
+
if (!current?.children) return Option.none();
|
|
200
|
+
if (typeof segment === "string") {
|
|
201
|
+
if (current.type !== "object") return Option.none();
|
|
202
|
+
current = current.children.find((child) => child.type === "property" && child.children !== void 0 && child.children[0]?.value === segment)?.children?.[1];
|
|
203
|
+
} else {
|
|
204
|
+
if (current.type !== "array") return Option.none();
|
|
205
|
+
current = current.children[segment];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return current ? Option.some(current) : Option.none();
|
|
209
|
+
}
|
|
210
|
+
function findNodeAtOffsetImpl(root, offset) {
|
|
211
|
+
if (offset < root.offset || offset >= root.offset + root.length) return Option.none();
|
|
212
|
+
if (!root.children) return Option.some(root);
|
|
213
|
+
for (const child of root.children) if (offset >= child.offset && offset < child.offset + child.length) return findNodeAtOffsetImpl(child, offset);
|
|
214
|
+
return Option.some(root);
|
|
215
|
+
}
|
|
216
|
+
function buildPath(node, targetOffset, currentPath) {
|
|
217
|
+
if (targetOffset < node.offset || targetOffset >= node.offset + node.length) return Option.none();
|
|
218
|
+
if (!node.children) return Option.some(currentPath);
|
|
219
|
+
if (node.type === "object") {
|
|
220
|
+
for (const prop of node.children) if (prop.type === "property" && prop.children !== void 0 && targetOffset >= prop.offset && targetOffset < prop.offset + prop.length) {
|
|
221
|
+
const key = prop.children[0]?.value;
|
|
222
|
+
const valuePath = [...currentPath, key];
|
|
223
|
+
const valueChild = prop.children[1];
|
|
224
|
+
if (valueChild && targetOffset >= valueChild.offset && targetOffset < valueChild.offset + valueChild.length) return buildPath(valueChild, targetOffset, valuePath);
|
|
225
|
+
return Option.some(valuePath);
|
|
226
|
+
}
|
|
227
|
+
} else if (node.type === "array") for (let i = 0; i < node.children.length; i++) {
|
|
228
|
+
const child = node.children[i];
|
|
229
|
+
if (targetOffset >= child.offset && targetOffset < child.offset + child.length) return buildPath(child, targetOffset, [...currentPath, i]);
|
|
230
|
+
}
|
|
231
|
+
return Option.some(currentPath);
|
|
232
|
+
}
|
|
233
|
+
function evaluateNode(node) {
|
|
234
|
+
switch (node.type) {
|
|
235
|
+
case "object": {
|
|
236
|
+
const obj = {};
|
|
237
|
+
if (node.children) {
|
|
238
|
+
for (const prop of node.children) if (prop.type === "property" && prop.children !== void 0 && prop.children.length === 2) {
|
|
239
|
+
const key = prop.children[0].value;
|
|
240
|
+
obj[key] = evaluateNode(prop.children[1]);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return obj;
|
|
244
|
+
}
|
|
245
|
+
case "array": return (node.children ?? []).map(evaluateNode);
|
|
246
|
+
case "property": return node.children?.[1] ? evaluateNode(node.children[1]) : void 0;
|
|
247
|
+
case "string":
|
|
248
|
+
case "number":
|
|
249
|
+
case "boolean":
|
|
250
|
+
case "null": return node.value;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
//#endregion
|
|
255
|
+
export { findNode, findNodeAtOffset, getNodePath, getNodeValue };
|
package/equality.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { parse } from "./parse.js";
|
|
2
|
+
import { Effect, Function } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/equality.ts
|
|
5
|
+
/**
|
|
6
|
+
* JSONC equality comparisons — semantic equivalence for JSONC documents.
|
|
7
|
+
*
|
|
8
|
+
* Compares parsed values ignoring comments, whitespace, formatting,
|
|
9
|
+
* and object key ordering.
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Deep-compare two plain JS values for structural equality (key-order independent for objects, order sensitive for arrays).
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
function deepEqual(a, b) {
|
|
19
|
+
if (a === b) return true;
|
|
20
|
+
if (a === null || b === null) return false;
|
|
21
|
+
if (typeof a !== typeof b) return false;
|
|
22
|
+
if (Array.isArray(a)) {
|
|
23
|
+
if (!Array.isArray(b)) return false;
|
|
24
|
+
if (a.length !== b.length) return false;
|
|
25
|
+
for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(b)) return false;
|
|
29
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
30
|
+
const aObj = a;
|
|
31
|
+
const bObj = b;
|
|
32
|
+
const aKeys = Object.keys(aObj);
|
|
33
|
+
const bKeys = Object.keys(bObj);
|
|
34
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
35
|
+
for (const key of aKeys) {
|
|
36
|
+
if (!Object.hasOwn(bObj, key)) return false;
|
|
37
|
+
if (!deepEqual(aObj[key], bObj[key])) return false;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Compare two JSONC strings for semantic equality.
|
|
45
|
+
*
|
|
46
|
+
* Supports {@link https://effect.website/docs/effect/function#dual | Function.dual}
|
|
47
|
+
* for both data-first and data-last (pipeline) usage.
|
|
48
|
+
*
|
|
49
|
+
* @param self - The first JSONC string.
|
|
50
|
+
* @param that - The second JSONC string.
|
|
51
|
+
*
|
|
52
|
+
* @returns `Effect<boolean, JsoncParseError>` — `true` when both strings parse to
|
|
53
|
+
* semantically equivalent values, `false` otherwise. Fails with
|
|
54
|
+
* {@link JsoncParseError} if either string is malformed.
|
|
55
|
+
*
|
|
56
|
+
* @remarks
|
|
57
|
+
* Both strings are parsed via {@link parse} and then deep-compared. The comparison
|
|
58
|
+
* ignores comments, whitespace, formatting, and object key ordering. Array order
|
|
59
|
+
* IS significant. Uses `Effect.all` internally, so the effect fails on the first
|
|
60
|
+
* parse error encountered.
|
|
61
|
+
*
|
|
62
|
+
* @see {@link equalsValue} — compare a JSONC string against an existing JS value
|
|
63
|
+
* @see {@link parse} — the underlying parser used for both strings
|
|
64
|
+
*
|
|
65
|
+
* @example Data-first comparison
|
|
66
|
+
* ```ts
|
|
67
|
+
* import { Effect } from "effect";
|
|
68
|
+
* import { equals } from "jsonc-effect";
|
|
69
|
+
*
|
|
70
|
+
* const result = Effect.runSync(
|
|
71
|
+
* equals('{ "a": 1, "b": 2 }', '{"b":2,"a":1}')
|
|
72
|
+
* );
|
|
73
|
+
* // result is true
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @example Key-order independence
|
|
77
|
+
* ```ts
|
|
78
|
+
* import { Effect } from "effect";
|
|
79
|
+
* import { equals } from "jsonc-effect";
|
|
80
|
+
*
|
|
81
|
+
* // Object key order does not matter
|
|
82
|
+
* const sameKeys = Effect.runSync(
|
|
83
|
+
* equals('{"z":1,"a":2}', '{"a":2,"z":1}')
|
|
84
|
+
* );
|
|
85
|
+
* // sameKeys is true
|
|
86
|
+
*
|
|
87
|
+
* // Array order DOES matter
|
|
88
|
+
* const differentOrder = Effect.runSync(
|
|
89
|
+
* equals('[1, 2]', '[2, 1]')
|
|
90
|
+
* );
|
|
91
|
+
* // differentOrder is false
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* @example Error handling
|
|
95
|
+
* ```ts
|
|
96
|
+
* import { Effect, Either } from "effect";
|
|
97
|
+
* import type { JsoncParseError } from "jsonc-effect";
|
|
98
|
+
* import { equals } from "jsonc-effect";
|
|
99
|
+
*
|
|
100
|
+
* const result: Either.Either<boolean, JsoncParseError> = Effect.runSync(
|
|
101
|
+
* Effect.either(equals('{ invalid }', '{}'))
|
|
102
|
+
* );
|
|
103
|
+
* // result is Either.left(JsoncParseError)
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* @privateRemarks
|
|
107
|
+
* Uses a simple recursive `deepEqual` helper rather than Effect's `Equal` module
|
|
108
|
+
* because the parsed values are plain JS objects and arrays, not Effect data types.
|
|
109
|
+
*
|
|
110
|
+
* @public
|
|
111
|
+
*/
|
|
112
|
+
const equals = Function.dual(2, (self, that) => Effect.map(Effect.all([parse(self), parse(that)]), ([a, b]) => deepEqual(a, b)));
|
|
113
|
+
/**
|
|
114
|
+
* Compare a JSONC string against a JavaScript value for semantic equality.
|
|
115
|
+
*
|
|
116
|
+
* Supports {@link https://effect.website/docs/effect/function#dual | Function.dual}
|
|
117
|
+
* for both data-first and data-last (pipeline) usage.
|
|
118
|
+
*
|
|
119
|
+
* @param self - The JSONC string to parse.
|
|
120
|
+
* @param value - The JavaScript value to compare against.
|
|
121
|
+
*
|
|
122
|
+
* @returns `Effect<boolean, JsoncParseError>` — `true` when the parsed JSONC
|
|
123
|
+
* is semantically equivalent to the provided value, `false` otherwise.
|
|
124
|
+
* Fails with {@link JsoncParseError} if the string is malformed.
|
|
125
|
+
*
|
|
126
|
+
* @remarks
|
|
127
|
+
* Only the JSONC string is parsed; the JS value is used as-is. This makes
|
|
128
|
+
* `equalsValue` useful for assertions and testing where the expected value
|
|
129
|
+
* is already a JS object. The comparison semantics are the same as
|
|
130
|
+
* {@link equals}: comments, whitespace, formatting, and object key ordering
|
|
131
|
+
* are ignored, while array order IS significant.
|
|
132
|
+
*
|
|
133
|
+
* @see {@link equals} — compare two JSONC strings against each other
|
|
134
|
+
* @see {@link parse} — the underlying parser
|
|
135
|
+
*
|
|
136
|
+
* @example Basic comparison
|
|
137
|
+
* ```ts
|
|
138
|
+
* import { Effect } from "effect";
|
|
139
|
+
* import { equalsValue } from "jsonc-effect";
|
|
140
|
+
*
|
|
141
|
+
* const result = Effect.runSync(
|
|
142
|
+
* equalsValue('{"port": 3000, "host": "localhost"}', { host: "localhost", port: 3000 })
|
|
143
|
+
* );
|
|
144
|
+
* // result is true
|
|
145
|
+
* ```
|
|
146
|
+
*
|
|
147
|
+
* @example Pipeline usage for testing
|
|
148
|
+
* ```ts
|
|
149
|
+
* import { Effect, pipe } from "effect";
|
|
150
|
+
* import { equalsValue } from "jsonc-effect";
|
|
151
|
+
*
|
|
152
|
+
* const jsonc = '{ "enabled": true, "count": 5 }';
|
|
153
|
+
* const expected = { enabled: true, count: 5 };
|
|
154
|
+
*
|
|
155
|
+
* const result = Effect.runSync(
|
|
156
|
+
* pipe(jsonc, equalsValue(expected))
|
|
157
|
+
* );
|
|
158
|
+
* // result is true
|
|
159
|
+
* ```
|
|
160
|
+
*
|
|
161
|
+
* @public
|
|
162
|
+
*/
|
|
163
|
+
const equalsValue = Function.dual(2, (self, value) => Effect.map(parse(self), (parsed) => deepEqual(parsed, value)));
|
|
164
|
+
|
|
165
|
+
//#endregion
|
|
166
|
+
export { equals, equalsValue };
|
package/errors.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { Data, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/errors.ts
|
|
4
|
+
/**
|
|
5
|
+
* JSONC error types using Effect's Data.TaggedError pattern.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Error codes representing specific JSONC parse failures.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* Each code maps to a distinct syntactic error the parser can encounter,
|
|
14
|
+
* from invalid symbols and number formats to missing delimiters and
|
|
15
|
+
* unexpected end-of-input conditions.
|
|
16
|
+
*
|
|
17
|
+
* @see {@link JsoncParseErrorDetail} — carries one of these codes alongside
|
|
18
|
+
* position information
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
const JsoncParseErrorCode = Schema.Literal("InvalidSymbol", "InvalidNumberFormat", "PropertyNameExpected", "ValueExpected", "ColonExpected", "CommaExpected", "CloseBraceExpected", "CloseBracketExpected", "EndOfFileExpected", "InvalidCommentToken", "UnexpectedEndOfComment", "UnexpectedEndOfString", "UnexpectedEndOfNumber", "InvalidUnicode", "InvalidEscapeCharacter", "InvalidCharacter");
|
|
23
|
+
/**
|
|
24
|
+
* Detail for a single parse error, including the error code, a human-readable
|
|
25
|
+
* message, and the exact position within the source document.
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
* - `code` — a {@link (JsoncParseErrorCode:type)} identifying the error kind.
|
|
29
|
+
* - `message` — a descriptive message suitable for display.
|
|
30
|
+
* - `offset` — zero-based character offset where the error occurred.
|
|
31
|
+
* - `length` — character length of the problematic span.
|
|
32
|
+
* - `startLine` — zero-based line number of the error.
|
|
33
|
+
* - `startCharacter` — zero-based column within `startLine`.
|
|
34
|
+
*
|
|
35
|
+
* @see {@link JsoncParseError} — aggregates an array of these details
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* import { JsoncParseErrorDetail } from "jsonc-effect";
|
|
40
|
+
*
|
|
41
|
+
* const detail = new JsoncParseErrorDetail({
|
|
42
|
+
* code: "ValueExpected",
|
|
43
|
+
* message: "Value expected",
|
|
44
|
+
* offset: 5,
|
|
45
|
+
* length: 1,
|
|
46
|
+
* startLine: 0,
|
|
47
|
+
* startCharacter: 5,
|
|
48
|
+
* });
|
|
49
|
+
*
|
|
50
|
+
* console.log(detail.code); // "ValueExpected"
|
|
51
|
+
* console.log(detail.offset); // 5
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* @public
|
|
55
|
+
*/
|
|
56
|
+
var JsoncParseErrorDetail = class extends Schema.Class("JsoncParseErrorDetail")({
|
|
57
|
+
code: JsoncParseErrorCode,
|
|
58
|
+
message: Schema.String,
|
|
59
|
+
offset: Schema.Number,
|
|
60
|
+
length: Schema.Number,
|
|
61
|
+
startLine: Schema.Number,
|
|
62
|
+
startCharacter: Schema.Number
|
|
63
|
+
}) {};
|
|
64
|
+
/**
|
|
65
|
+
* Base class for {@link JsoncParseError}; not intended to be constructed or
|
|
66
|
+
* caught directly — use `JsoncParseError` instead.
|
|
67
|
+
*
|
|
68
|
+
* @privateRemarks
|
|
69
|
+
* The `*Base` pattern is required because `Data.TaggedError` produces complex
|
|
70
|
+
* type signatures involving intersection types and branded generics that
|
|
71
|
+
* api-extractor cannot roll up into a single `.d.ts` bundle. Exporting the
|
|
72
|
+
* base separately lets the public `JsoncParseError` class extend it with
|
|
73
|
+
* concrete fields, giving api-extractor a simple class declaration to work
|
|
74
|
+
* with. It is tagged `@public` (rather than `@internal`) because it appears
|
|
75
|
+
* in `JsoncParseError`'s heritage clause in the public `.d.ts`, and API
|
|
76
|
+
* Extractor requires release tags to be compatible across a signature.
|
|
77
|
+
*
|
|
78
|
+
* @public
|
|
79
|
+
*/
|
|
80
|
+
const JsoncParseErrorBase = Data.TaggedError("JsoncParseError");
|
|
81
|
+
/**
|
|
82
|
+
* Error raised when JSONC parsing encounters one or more syntax errors.
|
|
83
|
+
*
|
|
84
|
+
* @remarks
|
|
85
|
+
* Contains the full source `text`, the `options` used for parsing, and an
|
|
86
|
+
* `errors` array of {@link JsoncParseErrorDetail} instances with precise
|
|
87
|
+
* position information for each problem found.
|
|
88
|
+
*
|
|
89
|
+
* @see {@link parse} — may fail with this error
|
|
90
|
+
* @see {@link parseTree} — may fail with this error
|
|
91
|
+
*
|
|
92
|
+
* @example Catching with `Effect.catchTag`
|
|
93
|
+
* ```ts
|
|
94
|
+
* import { Effect } from "effect";
|
|
95
|
+
* import { parse } from "jsonc-effect";
|
|
96
|
+
*
|
|
97
|
+
* const program = parse("{ invalid }").pipe(
|
|
98
|
+
* Effect.catchTag("JsoncParseError", (e) => {
|
|
99
|
+
* console.error(e.errors); // Array of JsoncParseErrorDetail
|
|
100
|
+
* return Effect.succeed({});
|
|
101
|
+
* }),
|
|
102
|
+
* );
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* @example Inspecting error details
|
|
106
|
+
* ```ts
|
|
107
|
+
* import { Effect } from "effect";
|
|
108
|
+
* import { parse } from "jsonc-effect";
|
|
109
|
+
*
|
|
110
|
+
* const program = parse("{ invalid }").pipe(
|
|
111
|
+
* Effect.catchTag("JsoncParseError", (e) => {
|
|
112
|
+
* for (const detail of e.errors) {
|
|
113
|
+
* console.error(
|
|
114
|
+
* `[${detail.code}] ${detail.message} at line ${detail.startLine}:${detail.startCharacter}`,
|
|
115
|
+
* );
|
|
116
|
+
* }
|
|
117
|
+
* return Effect.succeed({});
|
|
118
|
+
* }),
|
|
119
|
+
* );
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* @public
|
|
123
|
+
*/
|
|
124
|
+
var JsoncParseError = class extends JsoncParseErrorBase {
|
|
125
|
+
get message() {
|
|
126
|
+
const count = this.errors.length;
|
|
127
|
+
return `JSONC parse failed with ${count} error${count !== 1 ? "s" : ""}: ${this.errors.map((e) => e.message).join("; ")}`;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Base class for {@link JsoncNodeNotFoundError}; not intended to be
|
|
132
|
+
* constructed or caught directly — use `JsoncNodeNotFoundError` instead.
|
|
133
|
+
*
|
|
134
|
+
* @privateRemarks
|
|
135
|
+
* Uses the same `*Base` pattern as {@link JsoncParseErrorBase} to work
|
|
136
|
+
* around api-extractor's inability to roll up the complex type produced
|
|
137
|
+
* by `Data.TaggedError` into a single `.d.ts` declaration. Tagged `@public`
|
|
138
|
+
* for the same heritage-clause-compatibility reason as `JsoncParseErrorBase`.
|
|
139
|
+
*
|
|
140
|
+
* @public
|
|
141
|
+
*/
|
|
142
|
+
const JsoncNodeNotFoundErrorBase = Data.TaggedError("JsoncNodeNotFoundError");
|
|
143
|
+
/**
|
|
144
|
+
* Error raised when AST navigation fails to find a node at the given path.
|
|
145
|
+
*
|
|
146
|
+
* @remarks
|
|
147
|
+
* Contains the `path` that was searched and the `rootNodeType` of the tree
|
|
148
|
+
* that was traversed.
|
|
149
|
+
*
|
|
150
|
+
* @see {@link findNode} — may fail with this error
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```ts
|
|
154
|
+
* import { Effect } from "effect";
|
|
155
|
+
* import { parseTree, findNode } from "jsonc-effect";
|
|
156
|
+
*
|
|
157
|
+
* const program = parseTree('{ "a": 1 }').pipe(
|
|
158
|
+
* Effect.flatMap((root) => findNode(root, ["missing", "path"])),
|
|
159
|
+
* Effect.catchTag("JsoncNodeNotFoundError", (e) => {
|
|
160
|
+
* console.error(`Not found: [${e.path.join(", ")}] in ${e.rootNodeType}`);
|
|
161
|
+
* return Effect.succeed(undefined);
|
|
162
|
+
* }),
|
|
163
|
+
* );
|
|
164
|
+
* ```
|
|
165
|
+
*
|
|
166
|
+
* @public
|
|
167
|
+
*/
|
|
168
|
+
var JsoncNodeNotFoundError = class extends JsoncNodeNotFoundErrorBase {
|
|
169
|
+
get message() {
|
|
170
|
+
return `Node not found at path [${this.path.join(", ")}] in ${this.rootNodeType} node`;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* Base class for {@link JsoncModificationError}; not intended to be
|
|
175
|
+
* constructed or caught directly — use `JsoncModificationError` instead.
|
|
176
|
+
*
|
|
177
|
+
* @privateRemarks
|
|
178
|
+
* Uses the same `*Base` pattern as {@link JsoncParseErrorBase} to work
|
|
179
|
+
* around api-extractor's inability to roll up the complex type produced
|
|
180
|
+
* by `Data.TaggedError` into a single `.d.ts` declaration. Tagged `@public`
|
|
181
|
+
* for the same heritage-clause-compatibility reason as `JsoncParseErrorBase`.
|
|
182
|
+
*
|
|
183
|
+
* @public
|
|
184
|
+
*/
|
|
185
|
+
const JsoncModificationErrorBase = Data.TaggedError("JsoncModificationError");
|
|
186
|
+
/**
|
|
187
|
+
* Error raised when {@link modify} produces invalid edits or encounters
|
|
188
|
+
* an unsupported modification scenario.
|
|
189
|
+
*
|
|
190
|
+
* @remarks
|
|
191
|
+
* Contains the `path` where modification was attempted and a `reason`
|
|
192
|
+
* string explaining why it failed.
|
|
193
|
+
*
|
|
194
|
+
* @see {@link modify} — may fail with this error
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* import { Effect } from "effect";
|
|
199
|
+
* import { modify } from "jsonc-effect";
|
|
200
|
+
*
|
|
201
|
+
* const program = modify("{}", ["deep", "path"], 42).pipe(
|
|
202
|
+
* Effect.catchTag("JsoncModificationError", (e) => {
|
|
203
|
+
* console.error(`Failed at [${e.path.join(", ")}]: ${e.reason}`);
|
|
204
|
+
* return Effect.succeed([]);
|
|
205
|
+
* }),
|
|
206
|
+
* );
|
|
207
|
+
* ```
|
|
208
|
+
*
|
|
209
|
+
* @public
|
|
210
|
+
*/
|
|
211
|
+
var JsoncModificationError = class extends JsoncModificationErrorBase {
|
|
212
|
+
get message() {
|
|
213
|
+
return `Modification failed at path [${this.path.join(", ")}]: ${this.reason}`;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
//#endregion
|
|
218
|
+
export { JsoncModificationError, JsoncModificationErrorBase, JsoncNodeNotFoundError, JsoncNodeNotFoundErrorBase, JsoncParseError, JsoncParseErrorBase, JsoncParseErrorCode, JsoncParseErrorDetail };
|