jsonc-effect 0.2.1 → 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/scanner.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
//#region src/scanner.ts
|
|
2
|
+
const isWhitespace = (ch) => ch === 32 || ch === 9 || ch === 11 || ch === 12 || ch === 160 || ch === 65279;
|
|
3
|
+
const isLineBreak = (ch) => ch === 10 || ch === 13 || ch === 8232 || ch === 8233;
|
|
4
|
+
const isDigit = (ch) => ch >= 48 && ch <= 57;
|
|
5
|
+
/**
|
|
6
|
+
* Create a stateful {@link JsoncScanner} for the given JSONC string.
|
|
7
|
+
*
|
|
8
|
+
* @param text - JSONC string to tokenize
|
|
9
|
+
* @param ignoreTrivia - If `true`, the scanner automatically skips whitespace,
|
|
10
|
+
* line-break, and comment tokens so that only structural tokens are returned
|
|
11
|
+
* (default: `false`).
|
|
12
|
+
* @returns A stateful {@link JsoncScanner} positioned before the first token.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* When `ignoreTrivia` is `true` the scanner is suitable for building parsers
|
|
16
|
+
* that only care about structural tokens (`OpenBrace`, `String`, `Number`,
|
|
17
|
+
* etc.). Set it to `false` (the default) when you need to preserve comments
|
|
18
|
+
* or whitespace — for example in a formatter or a comment-stripping pass.
|
|
19
|
+
*
|
|
20
|
+
* @see {@link JsoncScanner} — the interface returned by this factory
|
|
21
|
+
* @see {@link parse} — higher-level API that uses a scanner internally
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* Tokenizing a JSONC string and printing each token:
|
|
25
|
+
* ```ts
|
|
26
|
+
* import type { JsoncSyntaxKind } from "jsonc-effect";
|
|
27
|
+
* import { createScanner } from "jsonc-effect";
|
|
28
|
+
*
|
|
29
|
+
* const scanner = createScanner('{ "name": "jsonc" }', true);
|
|
30
|
+
* let kind: JsoncSyntaxKind;
|
|
31
|
+
* do {
|
|
32
|
+
* kind = scanner.scan();
|
|
33
|
+
* console.log(kind, scanner.getTokenValue());
|
|
34
|
+
* } while (kind !== "EOF");
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @privateRemarks
|
|
38
|
+
* Ported from Microsoft's jsonc-parser (MIT), adapted to use string literal
|
|
39
|
+
* token types instead of numeric enums.
|
|
40
|
+
*
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
const createScanner = (text, ignoreTrivia = false) => {
|
|
44
|
+
const len = text.length;
|
|
45
|
+
let pos = 0;
|
|
46
|
+
let tokenOffset = 0;
|
|
47
|
+
let token = "Unknown";
|
|
48
|
+
let tokenValue = "";
|
|
49
|
+
let tokenError = "None";
|
|
50
|
+
let lineNumber = 0;
|
|
51
|
+
let lineStartOffset = 0;
|
|
52
|
+
let tokenStartLine = 0;
|
|
53
|
+
let tokenStartCharacter = 0;
|
|
54
|
+
const scanHexDigits = (count) => {
|
|
55
|
+
let value = 0;
|
|
56
|
+
for (let i = 0; i < count; i++) {
|
|
57
|
+
if (pos >= len) return -1;
|
|
58
|
+
const ch = text.charCodeAt(pos);
|
|
59
|
+
if (ch >= 48 && ch <= 57) value = value * 16 + (ch - 48);
|
|
60
|
+
else if (ch >= 65 && ch <= 70) value = value * 16 + (ch - 65 + 10);
|
|
61
|
+
else if (ch >= 97 && ch <= 102) value = value * 16 + (ch - 97 + 10);
|
|
62
|
+
else return -1;
|
|
63
|
+
pos++;
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
};
|
|
67
|
+
const scanString = () => {
|
|
68
|
+
let result = "";
|
|
69
|
+
pos++;
|
|
70
|
+
let start = pos;
|
|
71
|
+
while (pos < len) {
|
|
72
|
+
const ch = text.charCodeAt(pos);
|
|
73
|
+
if (ch === 34) {
|
|
74
|
+
result += text.substring(start, pos);
|
|
75
|
+
pos++;
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
if (ch === 92) {
|
|
79
|
+
result += text.substring(start, pos);
|
|
80
|
+
pos++;
|
|
81
|
+
if (pos >= len) {
|
|
82
|
+
tokenError = "UnexpectedEndOfString";
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
const escaped = text.charCodeAt(pos);
|
|
86
|
+
pos++;
|
|
87
|
+
switch (escaped) {
|
|
88
|
+
case 34:
|
|
89
|
+
result += "\"";
|
|
90
|
+
break;
|
|
91
|
+
case 92:
|
|
92
|
+
result += "\\";
|
|
93
|
+
break;
|
|
94
|
+
case 47:
|
|
95
|
+
result += "/";
|
|
96
|
+
break;
|
|
97
|
+
case 98:
|
|
98
|
+
result += "\b";
|
|
99
|
+
break;
|
|
100
|
+
case 102:
|
|
101
|
+
result += "\f";
|
|
102
|
+
break;
|
|
103
|
+
case 110:
|
|
104
|
+
result += "\n";
|
|
105
|
+
break;
|
|
106
|
+
case 114:
|
|
107
|
+
result += "\r";
|
|
108
|
+
break;
|
|
109
|
+
case 116:
|
|
110
|
+
result += " ";
|
|
111
|
+
break;
|
|
112
|
+
case 117: {
|
|
113
|
+
const value = scanHexDigits(4);
|
|
114
|
+
if (value >= 0) result += String.fromCharCode(value);
|
|
115
|
+
else tokenError = "InvalidUnicode";
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
default:
|
|
119
|
+
tokenError = "InvalidEscapeCharacter";
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
start = pos;
|
|
123
|
+
} else if (isLineBreak(ch)) {
|
|
124
|
+
tokenError = "UnexpectedEndOfString";
|
|
125
|
+
return result + text.substring(start, pos);
|
|
126
|
+
} else pos++;
|
|
127
|
+
}
|
|
128
|
+
tokenError = "UnexpectedEndOfString";
|
|
129
|
+
return result + text.substring(start, pos);
|
|
130
|
+
};
|
|
131
|
+
const scanNumber = () => {
|
|
132
|
+
const start = pos;
|
|
133
|
+
if (text.charCodeAt(pos) === 45) pos++;
|
|
134
|
+
if (text.charCodeAt(pos) === 48) pos++;
|
|
135
|
+
else {
|
|
136
|
+
if (!isDigit(text.charCodeAt(pos))) {
|
|
137
|
+
tokenError = "UnexpectedEndOfNumber";
|
|
138
|
+
return text.substring(start, pos);
|
|
139
|
+
}
|
|
140
|
+
pos++;
|
|
141
|
+
while (pos < len && isDigit(text.charCodeAt(pos))) pos++;
|
|
142
|
+
}
|
|
143
|
+
if (pos < len && text.charCodeAt(pos) === 46) {
|
|
144
|
+
pos++;
|
|
145
|
+
if (!isDigit(text.charCodeAt(pos))) {
|
|
146
|
+
tokenError = "UnexpectedEndOfNumber";
|
|
147
|
+
return text.substring(start, pos);
|
|
148
|
+
}
|
|
149
|
+
pos++;
|
|
150
|
+
while (pos < len && isDigit(text.charCodeAt(pos))) pos++;
|
|
151
|
+
}
|
|
152
|
+
if (pos < len && (text.charCodeAt(pos) === 69 || text.charCodeAt(pos) === 101)) {
|
|
153
|
+
pos++;
|
|
154
|
+
if (pos < len && (text.charCodeAt(pos) === 43 || text.charCodeAt(pos) === 45)) pos++;
|
|
155
|
+
if (!isDigit(text.charCodeAt(pos))) {
|
|
156
|
+
tokenError = "UnexpectedEndOfNumber";
|
|
157
|
+
return text.substring(start, pos);
|
|
158
|
+
}
|
|
159
|
+
pos++;
|
|
160
|
+
while (pos < len && isDigit(text.charCodeAt(pos))) pos++;
|
|
161
|
+
}
|
|
162
|
+
return text.substring(start, pos);
|
|
163
|
+
};
|
|
164
|
+
const scan = () => {
|
|
165
|
+
tokenValue = "";
|
|
166
|
+
tokenError = "None";
|
|
167
|
+
if (pos >= len) {
|
|
168
|
+
tokenOffset = len;
|
|
169
|
+
tokenStartLine = lineNumber;
|
|
170
|
+
tokenStartCharacter = pos - lineStartOffset;
|
|
171
|
+
token = "EOF";
|
|
172
|
+
return token;
|
|
173
|
+
}
|
|
174
|
+
let ch = text.charCodeAt(pos);
|
|
175
|
+
if (isWhitespace(ch)) {
|
|
176
|
+
tokenOffset = pos;
|
|
177
|
+
tokenStartLine = lineNumber;
|
|
178
|
+
tokenStartCharacter = pos - lineStartOffset;
|
|
179
|
+
do {
|
|
180
|
+
pos++;
|
|
181
|
+
ch = pos < len ? text.charCodeAt(pos) : 0;
|
|
182
|
+
} while (isWhitespace(ch));
|
|
183
|
+
tokenValue = text.substring(tokenOffset, pos);
|
|
184
|
+
if (ignoreTrivia) return scan();
|
|
185
|
+
token = "Trivia";
|
|
186
|
+
return token;
|
|
187
|
+
}
|
|
188
|
+
if (isLineBreak(ch)) {
|
|
189
|
+
tokenOffset = pos;
|
|
190
|
+
tokenStartLine = lineNumber;
|
|
191
|
+
tokenStartCharacter = pos - lineStartOffset;
|
|
192
|
+
pos++;
|
|
193
|
+
if (ch === 13 && pos < len && text.charCodeAt(pos) === 10) pos++;
|
|
194
|
+
lineNumber++;
|
|
195
|
+
lineStartOffset = pos;
|
|
196
|
+
tokenValue = text.substring(tokenOffset, pos);
|
|
197
|
+
if (ignoreTrivia) return scan();
|
|
198
|
+
token = "LineBreak";
|
|
199
|
+
return token;
|
|
200
|
+
}
|
|
201
|
+
tokenOffset = pos;
|
|
202
|
+
tokenStartLine = lineNumber;
|
|
203
|
+
tokenStartCharacter = pos - lineStartOffset;
|
|
204
|
+
switch (ch) {
|
|
205
|
+
case 123:
|
|
206
|
+
pos++;
|
|
207
|
+
tokenValue = "{";
|
|
208
|
+
token = "OpenBrace";
|
|
209
|
+
return token;
|
|
210
|
+
case 125:
|
|
211
|
+
pos++;
|
|
212
|
+
tokenValue = "}";
|
|
213
|
+
token = "CloseBrace";
|
|
214
|
+
return token;
|
|
215
|
+
case 91:
|
|
216
|
+
pos++;
|
|
217
|
+
tokenValue = "[";
|
|
218
|
+
token = "OpenBracket";
|
|
219
|
+
return token;
|
|
220
|
+
case 93:
|
|
221
|
+
pos++;
|
|
222
|
+
tokenValue = "]";
|
|
223
|
+
token = "CloseBracket";
|
|
224
|
+
return token;
|
|
225
|
+
case 58:
|
|
226
|
+
pos++;
|
|
227
|
+
tokenValue = ":";
|
|
228
|
+
token = "Colon";
|
|
229
|
+
return token;
|
|
230
|
+
case 44:
|
|
231
|
+
pos++;
|
|
232
|
+
tokenValue = ",";
|
|
233
|
+
token = "Comma";
|
|
234
|
+
return token;
|
|
235
|
+
case 34:
|
|
236
|
+
tokenValue = scanString();
|
|
237
|
+
token = "String";
|
|
238
|
+
return token;
|
|
239
|
+
case 47: {
|
|
240
|
+
const nextCh = pos + 1 < len ? text.charCodeAt(pos + 1) : 0;
|
|
241
|
+
if (nextCh === 47) {
|
|
242
|
+
pos += 2;
|
|
243
|
+
while (pos < len && !isLineBreak(text.charCodeAt(pos))) pos++;
|
|
244
|
+
tokenValue = text.substring(tokenOffset, pos);
|
|
245
|
+
if (ignoreTrivia) return scan();
|
|
246
|
+
token = "LineComment";
|
|
247
|
+
return token;
|
|
248
|
+
}
|
|
249
|
+
if (nextCh === 42) {
|
|
250
|
+
pos += 2;
|
|
251
|
+
const safeLen = len - 1;
|
|
252
|
+
let commentClosed = false;
|
|
253
|
+
while (pos < safeLen) {
|
|
254
|
+
const cch = text.charCodeAt(pos);
|
|
255
|
+
if (isLineBreak(cch)) {
|
|
256
|
+
if (cch === 13 && pos + 1 < len && text.charCodeAt(pos + 1) === 10) pos++;
|
|
257
|
+
pos++;
|
|
258
|
+
lineNumber++;
|
|
259
|
+
lineStartOffset = pos;
|
|
260
|
+
} else if (cch === 42 && text.charCodeAt(pos + 1) === 47) {
|
|
261
|
+
pos += 2;
|
|
262
|
+
commentClosed = true;
|
|
263
|
+
break;
|
|
264
|
+
} else pos++;
|
|
265
|
+
}
|
|
266
|
+
if (!commentClosed) {
|
|
267
|
+
pos = len;
|
|
268
|
+
tokenError = "UnexpectedEndOfComment";
|
|
269
|
+
}
|
|
270
|
+
tokenValue = text.substring(tokenOffset, pos);
|
|
271
|
+
if (ignoreTrivia) return scan();
|
|
272
|
+
token = "BlockComment";
|
|
273
|
+
return token;
|
|
274
|
+
}
|
|
275
|
+
pos++;
|
|
276
|
+
tokenValue = text.substring(tokenOffset, pos);
|
|
277
|
+
token = "Unknown";
|
|
278
|
+
tokenError = "InvalidCharacter";
|
|
279
|
+
return token;
|
|
280
|
+
}
|
|
281
|
+
case 45:
|
|
282
|
+
if (pos + 1 < len && isDigit(text.charCodeAt(pos + 1))) {
|
|
283
|
+
tokenValue = scanNumber();
|
|
284
|
+
token = "Number";
|
|
285
|
+
return token;
|
|
286
|
+
}
|
|
287
|
+
pos++;
|
|
288
|
+
tokenValue = "-";
|
|
289
|
+
token = "Unknown";
|
|
290
|
+
tokenError = "InvalidSymbol";
|
|
291
|
+
return token;
|
|
292
|
+
default:
|
|
293
|
+
if (isDigit(ch)) {
|
|
294
|
+
tokenValue = scanNumber();
|
|
295
|
+
token = "Number";
|
|
296
|
+
return token;
|
|
297
|
+
}
|
|
298
|
+
if (ch >= 97 && ch <= 122) {
|
|
299
|
+
const start = pos;
|
|
300
|
+
pos++;
|
|
301
|
+
while (pos < len) {
|
|
302
|
+
const kch = text.charCodeAt(pos);
|
|
303
|
+
if (kch >= 97 && kch <= 122) pos++;
|
|
304
|
+
else break;
|
|
305
|
+
}
|
|
306
|
+
tokenValue = text.substring(start, pos);
|
|
307
|
+
switch (tokenValue) {
|
|
308
|
+
case "true":
|
|
309
|
+
token = "True";
|
|
310
|
+
return token;
|
|
311
|
+
case "false":
|
|
312
|
+
token = "False";
|
|
313
|
+
return token;
|
|
314
|
+
case "null":
|
|
315
|
+
token = "Null";
|
|
316
|
+
return token;
|
|
317
|
+
default:
|
|
318
|
+
token = "Unknown";
|
|
319
|
+
tokenError = "InvalidSymbol";
|
|
320
|
+
return token;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
pos++;
|
|
324
|
+
tokenValue = text.substring(tokenOffset, pos);
|
|
325
|
+
token = "Unknown";
|
|
326
|
+
tokenError = "InvalidCharacter";
|
|
327
|
+
return token;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
return {
|
|
331
|
+
scan,
|
|
332
|
+
getToken: () => token,
|
|
333
|
+
getTokenValue: () => tokenValue,
|
|
334
|
+
getTokenOffset: () => tokenOffset,
|
|
335
|
+
getTokenLength: () => pos - tokenOffset,
|
|
336
|
+
getTokenStartLine: () => tokenStartLine,
|
|
337
|
+
getTokenStartCharacter: () => tokenStartCharacter,
|
|
338
|
+
getTokenError: () => tokenError,
|
|
339
|
+
getPosition: () => pos,
|
|
340
|
+
setPosition: (newPos) => {
|
|
341
|
+
pos = newPos;
|
|
342
|
+
tokenValue = "";
|
|
343
|
+
token = "Unknown";
|
|
344
|
+
tokenError = "None";
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
//#endregion
|
|
350
|
+
export { createScanner };
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { parse } from "./parse.js";
|
|
2
|
+
import { Effect, ParseResult, Schema } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/schema-integration.ts
|
|
5
|
+
/**
|
|
6
|
+
* Schema-based JSONC parsing pipelines.
|
|
7
|
+
*
|
|
8
|
+
* Transforms JSONC strings directly into typed domain objects
|
|
9
|
+
* using Schema.transformOrFail and Schema.compose.
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Pre-built `Schema<unknown, string>` that decodes a JSONC string into an
|
|
15
|
+
* unknown JavaScript value using default parse options.
|
|
16
|
+
*
|
|
17
|
+
* This is the first stage of a typical parsing pipeline:
|
|
18
|
+
*
|
|
19
|
+
* ```
|
|
20
|
+
* JSONC string --JsoncFromString--> unknown --YourSchema--> A
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* Default parse options are used (comments allowed, trailing commas
|
|
25
|
+
* allowed). The encode direction uses `JSON.stringify` with 2-space
|
|
26
|
+
* indentation, which means comments present in the original JSONC
|
|
27
|
+
* input are not preserved during a round-trip encode.
|
|
28
|
+
*
|
|
29
|
+
* For custom parse options, use {@link makeJsoncFromString} instead.
|
|
30
|
+
*
|
|
31
|
+
* @see {@link makeJsoncFromString} to create a schema with custom options.
|
|
32
|
+
* @see {@link makeJsoncSchema} to compose JSONC parsing with a domain schema
|
|
33
|
+
* in one step.
|
|
34
|
+
*
|
|
35
|
+
* @example Decode a JSONC string
|
|
36
|
+
* ```ts
|
|
37
|
+
* import { Schema } from "effect";
|
|
38
|
+
* import { JsoncFromString } from "jsonc-effect";
|
|
39
|
+
*
|
|
40
|
+
* const value: unknown = Schema.decodeUnknownSync(JsoncFromString)(
|
|
41
|
+
* '{ "key": 42 // comment\n}',
|
|
42
|
+
* );
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example Compose with a domain schema
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { Schema } from "effect";
|
|
48
|
+
* import { JsoncFromString } from "jsonc-effect";
|
|
49
|
+
*
|
|
50
|
+
* const MyConfig = Schema.Struct({
|
|
51
|
+
* name: Schema.String,
|
|
52
|
+
* version: Schema.Number,
|
|
53
|
+
* });
|
|
54
|
+
*
|
|
55
|
+
* const MyConfigFromJsonc = Schema.compose(JsoncFromString, MyConfig);
|
|
56
|
+
* const config = Schema.decodeUnknownSync(MyConfigFromJsonc)(
|
|
57
|
+
* '{ "name": "app", "version": 1 }',
|
|
58
|
+
* );
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* @public
|
|
62
|
+
*/
|
|
63
|
+
const JsoncFromString = makeJsoncFromString();
|
|
64
|
+
/**
|
|
65
|
+
* Create a `Schema<unknown, string>` that decodes JSONC with custom
|
|
66
|
+
* parse options.
|
|
67
|
+
*
|
|
68
|
+
* @param options - Partial {@link JsoncParseOptions} controlling comment
|
|
69
|
+
* handling and trailing-comma tolerance.
|
|
70
|
+
* @returns A `Schema<unknown, string>` configured with the given options.
|
|
71
|
+
*
|
|
72
|
+
* @remarks
|
|
73
|
+
* The encode direction uses `JSON.stringify` with 2-space indentation,
|
|
74
|
+
* which produces standard JSON. Comments present in the original JSONC
|
|
75
|
+
* input are not preserved during a round-trip encode.
|
|
76
|
+
*
|
|
77
|
+
* @see {@link JsoncFromString} for a zero-config default instance.
|
|
78
|
+
* @see {@link makeJsoncSchema} to compose JSONC parsing with a domain
|
|
79
|
+
* schema in one step.
|
|
80
|
+
*
|
|
81
|
+
* @example Strict mode (no comments, no trailing commas)
|
|
82
|
+
* ```ts
|
|
83
|
+
* import { Schema } from "effect";
|
|
84
|
+
* import { makeJsoncFromString } from "jsonc-effect";
|
|
85
|
+
*
|
|
86
|
+
* const StrictJsoncFromString = makeJsoncFromString({
|
|
87
|
+
* disallowComments: true,
|
|
88
|
+
* allowTrailingComma: false,
|
|
89
|
+
* });
|
|
90
|
+
*
|
|
91
|
+
* const value: unknown = Schema.decodeUnknownSync(StrictJsoncFromString)(
|
|
92
|
+
* '{ "key": 42 }',
|
|
93
|
+
* );
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* @privateRemarks
|
|
97
|
+
* Internally delegates to `Schema.transformOrFail` to wire up the decode
|
|
98
|
+
* and encode directions.
|
|
99
|
+
*
|
|
100
|
+
* @public
|
|
101
|
+
*/
|
|
102
|
+
function makeJsoncFromString(options) {
|
|
103
|
+
return Schema.transformOrFail(Schema.String, Schema.Unknown, {
|
|
104
|
+
strict: true,
|
|
105
|
+
decode: (input, _options, ast) => {
|
|
106
|
+
const program = options ? parse(input, options) : parse(input);
|
|
107
|
+
return Effect.mapError(program, (parseError) => new ParseResult.Type(ast, input, parseError.message));
|
|
108
|
+
},
|
|
109
|
+
encode: (value) => ParseResult.succeed(JSON.stringify(value, null, 2))
|
|
110
|
+
}).annotations({
|
|
111
|
+
title: "JsoncFromString",
|
|
112
|
+
description: "Parse a JSONC string into an unknown JavaScript value"
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Create a composed `Schema<A, string>` that parses a JSONC string and
|
|
117
|
+
* validates the result against a target domain schema in one step.
|
|
118
|
+
*
|
|
119
|
+
* @param targetSchema - The domain schema to validate the parsed value
|
|
120
|
+
* against. Its input type `I` must be assignable from `unknown`.
|
|
121
|
+
* @param options - Optional partial {@link JsoncParseOptions} forwarded to
|
|
122
|
+
* the underlying JSONC parser.
|
|
123
|
+
* @returns A `Schema<A, string>` that goes directly from a JSONC string
|
|
124
|
+
* to a fully validated domain value of type `A`.
|
|
125
|
+
*
|
|
126
|
+
* @remarks
|
|
127
|
+
* Internally composes two schema stages:
|
|
128
|
+
*
|
|
129
|
+
* 1. JSONC string to `unknown` (via {@link makeJsoncFromString}).
|
|
130
|
+
* 2. `unknown` to `A` (via the provided `targetSchema`).
|
|
131
|
+
*
|
|
132
|
+
* This avoids the need to manually call `Schema.compose`.
|
|
133
|
+
*
|
|
134
|
+
* @see {@link makeJsoncFromString} for the first stage of the pipeline.
|
|
135
|
+
*
|
|
136
|
+
* @example Typed configuration from JSONC
|
|
137
|
+
* ```ts
|
|
138
|
+
* import { Schema } from "effect";
|
|
139
|
+
* import { makeJsoncSchema } from "jsonc-effect";
|
|
140
|
+
*
|
|
141
|
+
* const MyConfig = Schema.Struct({
|
|
142
|
+
* name: Schema.String,
|
|
143
|
+
* version: Schema.Number,
|
|
144
|
+
* });
|
|
145
|
+
*
|
|
146
|
+
* const MyConfigFromJsonc = makeJsoncSchema(MyConfig);
|
|
147
|
+
* const config = Schema.decodeUnknownSync(MyConfigFromJsonc)(
|
|
148
|
+
* '{ "name": "app", "version": 1 }',
|
|
149
|
+
* );
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* @example With custom parse options
|
|
153
|
+
* ```ts
|
|
154
|
+
* import { Schema } from "effect";
|
|
155
|
+
* import { makeJsoncSchema } from "jsonc-effect";
|
|
156
|
+
*
|
|
157
|
+
* const MyConfig = Schema.Struct({ debug: Schema.Boolean });
|
|
158
|
+
*
|
|
159
|
+
* const StrictConfig = makeJsoncSchema(MyConfig, {
|
|
160
|
+
* disallowComments: true,
|
|
161
|
+
* allowTrailingComma: false,
|
|
162
|
+
* });
|
|
163
|
+
*
|
|
164
|
+
* const config = Schema.decodeUnknownSync(StrictConfig)(
|
|
165
|
+
* '{ "debug": true }',
|
|
166
|
+
* );
|
|
167
|
+
* ```
|
|
168
|
+
*
|
|
169
|
+
* @privateRemarks
|
|
170
|
+
* Uses `Schema.compose` internally to chain the JSONC-from-string schema
|
|
171
|
+
* with the provided target schema.
|
|
172
|
+
*
|
|
173
|
+
* @public
|
|
174
|
+
*/
|
|
175
|
+
const makeJsoncSchema = (targetSchema, options) => Schema.compose(makeJsoncFromString(options), targetSchema);
|
|
176
|
+
|
|
177
|
+
//#endregion
|
|
178
|
+
export { JsoncFromString, makeJsoncFromString, makeJsoncSchema };
|