@jarenjs/json 0.9.2 → 0.34.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/ARCHITECTURE.md +86 -13
- package/README.md +248 -23
- package/dist/types/canonical.d.ts +37 -0
- package/dist/types/cow.d.ts +28 -0
- package/dist/types/errors.d.ts +45 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/jslt/errors.d.ts +15 -8
- package/dist/types/jslt/index.d.ts +22 -0
- package/dist/types/jslt/packs/finance.d.ts +119 -0
- package/dist/types/jslt/packs/index.d.ts +310 -0
- package/dist/types/jslt/packs/math.d.ts +159 -0
- package/dist/types/jslt/packs/stats.d.ts +48 -0
- package/dist/types/jslt/registry.d.ts +65 -0
- package/dist/types/jtlt/errors.d.ts +3 -6
- package/dist/types/option-variants.d.ts +29 -0
- package/dist/types/patch.d.ts +214 -0
- package/dist/types/path.d.ts +139 -9
- package/dist/types/pointer.d.ts +100 -9
- package/dist/types/query/compile.d.ts +12 -0
- package/dist/types/query/errors.d.ts +72 -8
- package/dist/types/query/index.d.ts +317 -25
- package/dist/types/query/normalize.d.ts +24 -0
- package/dist/types/query/operators.d.ts +241 -1
- package/dist/types/query/runtime.d.ts +5 -8
- package/dist/types/query/types.d.ts +34 -0
- package/dist/types/segments.d.ts +31 -0
- package/dist/types/write.d.ts +204 -0
- package/dist/types/xquery/parse.d.ts +2 -3
- package/docs/JSLT-FORMAT.md +74 -3
- package/docs/JSLT-PRELUDE.md +1 -1
- package/docs/QUERY-FORMAT.md +695 -33
- package/package.json +18 -4
- package/schemas/geojson.draft-07.schema.json +323 -0
- package/schemas/geojson.jaren.schema.json +863 -0
- package/schemas/geojson.schema.json +172 -0
- package/schemas/jaren-jslt.authoring.schema.json +142 -0
- package/schemas/jaren-jslt.draft-07.schema.json +152 -11
- package/schemas/jaren-jslt.llm-profile.schema.json +782 -0
- package/schemas/jaren-jslt.schema.json +152 -11
- package/schemas/jaren-query.draft-07.schema.json +152 -11
- package/schemas/jaren-query.llm-profile.schema.json +619 -0
- package/schemas/jaren-query.schema.json +82 -15
- package/src/basic.js +1 -1
- package/src/canonical.js +170 -0
- package/src/cow.js +106 -0
- package/src/errors.js +68 -0
- package/src/index.js +3 -0
- package/src/jslt/dispatch.js +178 -28
- package/src/jslt/errors.js +19 -14
- package/src/jslt/index.js +37 -29
- package/src/jslt/packs/finance.js +49 -0
- package/src/jslt/packs/index.js +18 -0
- package/src/jslt/packs/math.js +46 -0
- package/src/jslt/packs/stats.js +65 -0
- package/src/jslt/registry.js +200 -0
- package/src/jslt/stylesheet.js +14 -23
- package/src/jtlt/desugar.js +2 -3
- package/src/jtlt/errors.js +6 -12
- package/src/jtlt/index.js +12 -29
- package/src/jtlt/template.js +9 -18
- package/src/option-variants.js +54 -0
- package/src/patch.js +1052 -0
- package/src/path.js +319 -52
- package/src/pointer.js +225 -44
- package/src/query/compile.js +790 -75
- package/src/query/errors.js +72 -12
- package/src/query/index.js +274 -42
- package/src/query/normalize.js +489 -78
- package/src/query/operators.js +620 -23
- package/src/query/runtime.js +5 -19
- package/src/query/types.js +213 -0
- package/src/segments.js +409 -64
- package/src/write.js +660 -0
- package/src/xquery/parse.js +37 -53
|
@@ -64,9 +64,10 @@
|
|
|
64
64
|
"format": "json-path"
|
|
65
65
|
},
|
|
66
66
|
"variablePathString": {
|
|
67
|
-
"description": "A variable-rooted path: '$name' with name matching [A-Za-z_][A-Za-z0-9_]*, optionally followed by RFC 9535 segments.
|
|
67
|
+
"description": "A variable-rooted path: '$name' with name matching [A-Za-z_][A-Za-z0-9_]*, optionally followed by RFC 9535 segments. The pattern checks the head; the json-path-segments format asserts the full segment grammar where format assertion is enabled (the compiler enforces it either way, JQ0004).",
|
|
68
68
|
"type": "string",
|
|
69
|
-
"pattern": "^\\$[A-Za-z_][A-Za-z0-9_]*($|[.\\[])"
|
|
69
|
+
"pattern": "^\\$[A-Za-z_][A-Za-z0-9_]*($|[.\\[])",
|
|
70
|
+
"format": "json-path-segments"
|
|
70
71
|
},
|
|
71
72
|
"arrayConstructor": {
|
|
72
73
|
"description": "An array in expression position constructs an array; element results are concatenated per XQuery sequence flattening.",
|
|
@@ -79,6 +80,7 @@
|
|
|
79
80
|
{ "$ref": "#/$defs/mapConstructor" },
|
|
80
81
|
{ "$ref": "#/$defs/constPhrase" },
|
|
81
82
|
{ "$ref": "#/$defs/mapPhrase" },
|
|
83
|
+
{ "$ref": "#/$defs/callPhrase" },
|
|
82
84
|
{ "$ref": "#/$defs/flworPhrase" },
|
|
83
85
|
{ "$ref": "#/$defs/somePhrase" },
|
|
84
86
|
{ "$ref": "#/$defs/everyPhrase" },
|
|
@@ -88,6 +90,8 @@
|
|
|
88
90
|
{ "$ref": "#/$defs/nonEmptyVariadicOperatorPhrase" },
|
|
89
91
|
{ "$ref": "#/$defs/conditionalArityOperatorPhrase" },
|
|
90
92
|
{ "$ref": "#/$defs/stringJoinPhrase" },
|
|
93
|
+
{ "$ref": "#/$defs/geohashPhrase" },
|
|
94
|
+
{ "$ref": "#/$defs/ternaryOperatorPhrase" },
|
|
91
95
|
{ "$ref": "#/$defs/replacePhrase" },
|
|
92
96
|
{ "$ref": "#/$defs/schemaOperatorPhrase" }
|
|
93
97
|
]
|
|
@@ -128,10 +132,25 @@
|
|
|
128
132
|
"minItems": 2,
|
|
129
133
|
"maxItems": 2
|
|
130
134
|
},
|
|
135
|
+
"callPhrase": {
|
|
136
|
+
"description": "A registered trusted host function call: ['name', ...argument expressions]. The name must be registered at compile time (options.functions, JQ0010); sequences cross the boundary as arrays, a returned undefined is the empty sequence, a throwing function is JQ2010.",
|
|
137
|
+
"type": "object",
|
|
138
|
+
"properties": {
|
|
139
|
+
"$call": {
|
|
140
|
+
"type": "array",
|
|
141
|
+
"minItems": 1,
|
|
142
|
+
"prefixItems": [{ "type": "string", "minLength": 1 }],
|
|
143
|
+
"items": { "$ref": "#/$defs/expression" }
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
"required": ["$call"],
|
|
147
|
+
"additionalProperties": false
|
|
148
|
+
},
|
|
131
149
|
"flworPhrase": {
|
|
132
|
-
"description": "FLWOR phrase. Clauses apply in the fixed semantic order $for, $let, $as, $where, $groupby, $orderby, $count, $return regardless of JSON key order (D7); interleavings are expressed by nesting phrases. $return is required, plus at least one of $for/$let.",
|
|
150
|
+
"description": "FLWOR phrase. Clauses apply in the fixed semantic order $fold, $for, $let, $as, $where, $groupby, $orderby, $count, $return regardless of JSON key order (D7); interleavings are expressed by nesting phrases. $return is required, plus at least one of $fold/$for/$let. With $fold the phrase evaluates to the final accumulator instead of the collected $return sequence.",
|
|
133
151
|
"type": "object",
|
|
134
152
|
"properties": {
|
|
153
|
+
"$fold": { "$ref": "#/$defs/foldBinding" },
|
|
135
154
|
"$for": { "$ref": "#/$defs/forBindings" },
|
|
136
155
|
"$let": { "$ref": "#/$defs/bindingMap" },
|
|
137
156
|
"$as": { "$ref": "#/$defs/asClause" },
|
|
@@ -144,16 +163,25 @@
|
|
|
144
163
|
"required": ["$return"],
|
|
145
164
|
"anyOf": [
|
|
146
165
|
{ "required": ["$for"] },
|
|
147
|
-
{ "required": ["$let"] }
|
|
166
|
+
{ "required": ["$let"] },
|
|
167
|
+
{ "required": ["$fold"] }
|
|
148
168
|
],
|
|
149
169
|
"additionalProperties": false
|
|
150
170
|
},
|
|
171
|
+
"foldBinding": {
|
|
172
|
+
"description": "The $fold accumulator: exactly one variable name mapped to the accumulator's initial value, evaluated once in the enclosing scope before the tuple stream starts. $return then names the accumulator's next value per surviving tuple.",
|
|
173
|
+
"type": "object",
|
|
174
|
+
"minProperties": 1,
|
|
175
|
+
"maxProperties": 1,
|
|
176
|
+
"propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
|
|
177
|
+
"additionalProperties": { "$ref": "#/$defs/expression" }
|
|
178
|
+
},
|
|
151
179
|
"variableName": {
|
|
152
180
|
"type": "string",
|
|
153
181
|
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
|
|
154
182
|
},
|
|
155
183
|
"forBindings": {
|
|
156
|
-
"description": "Iteration bindings: variable name to source expression or extended
|
|
184
|
+
"description": "Iteration bindings: variable name to source expression or extended $in binding. Bindings nest left-to-right in document key order and may be correlated; key-order-hostile producers should nest phrases instead.",
|
|
157
185
|
"type": "object",
|
|
158
186
|
"minProperties": 1,
|
|
159
187
|
"propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
|
|
@@ -166,13 +194,17 @@
|
|
|
166
194
|
]
|
|
167
195
|
},
|
|
168
196
|
"extendedForBinding": {
|
|
169
|
-
"description": "Extended $for binding
|
|
197
|
+
"description": "Extended $for binding. $in is the source; $at binds the 0-based (D6) position variable; $allowing-empty emits one tuple with the variable bound to the empty sequence (position -1) when the source yields no tuple; $window with $size, and optionally $step, iterates tumbling or sliding windows instead of items. Only $in is structurally required: the compiler rejects $size/$step without $window, and $window without $size (JQ0003).",
|
|
170
198
|
"type": "object",
|
|
171
199
|
"properties": {
|
|
172
200
|
"$in": { "$ref": "#/$defs/expression" },
|
|
173
|
-
"$at": { "$ref": "#/$defs/variableName" }
|
|
201
|
+
"$at": { "$ref": "#/$defs/variableName" },
|
|
202
|
+
"$allowing-empty": { "type": "boolean" },
|
|
203
|
+
"$window": { "enum": ["tumbling", "sliding"] },
|
|
204
|
+
"$size": { "type": "integer", "minimum": 1 },
|
|
205
|
+
"$step": { "type": "integer", "minimum": 1 }
|
|
174
206
|
},
|
|
175
|
-
"required": ["$in"
|
|
207
|
+
"required": ["$in"],
|
|
176
208
|
"additionalProperties": false
|
|
177
209
|
},
|
|
178
210
|
"bindingMap": {
|
|
@@ -209,12 +241,13 @@
|
|
|
209
241
|
]
|
|
210
242
|
},
|
|
211
243
|
"orderByKeySpec": {
|
|
212
|
-
"description": "Explicit order key: direction defaults to 'asc', empty-sequence placement to 'least'.",
|
|
244
|
+
"description": "Explicit order key: direction defaults to 'asc', empty-sequence placement to 'least'. A $collation names a registered pure compare function (options.collations, JQ0010) applied to string keys; the default stays code-point order.",
|
|
213
245
|
"type": "object",
|
|
214
246
|
"properties": {
|
|
215
247
|
"$key": { "$ref": "#/$defs/expression" },
|
|
216
248
|
"$dir": { "enum": ["asc", "desc"] },
|
|
217
|
-
"$empty": { "enum": ["least", "greatest"] }
|
|
249
|
+
"$empty": { "enum": ["least", "greatest"] },
|
|
250
|
+
"$collation": { "type": "string", "minLength": 1 }
|
|
218
251
|
},
|
|
219
252
|
"required": ["$key"],
|
|
220
253
|
"additionalProperties": false
|
|
@@ -248,10 +281,15 @@
|
|
|
248
281
|
"enum": [
|
|
249
282
|
"$exists", "$empty", "$not", "$neg",
|
|
250
283
|
"$count", "$sum", "$avg", "$min", "$max",
|
|
251
|
-
"$distinct", "$reverse", "$sort", "$head", "$tail",
|
|
284
|
+
"$distinct", "$reverse", "$sort", "$head", "$tail", "$entries", "$from-entries",
|
|
252
285
|
"$upper", "$lower", "$string-length", "$normalize-space",
|
|
253
286
|
"$string", "$number", "$boolean",
|
|
254
|
-
"$is-string", "$is-number", "$is-boolean", "$is-null", "$is-array", "$is-object"
|
|
287
|
+
"$is-string", "$is-number", "$is-boolean", "$is-null", "$is-array", "$is-object",
|
|
288
|
+
"$is-date", "$is-time", "$is-datetime", "$is-duration",
|
|
289
|
+
"$year", "$month", "$day", "$hours", "$minutes", "$seconds",
|
|
290
|
+
"$offset", "$epoch", "$datetime",
|
|
291
|
+
"$week", "$week-year", "$quarter", "$weekday",
|
|
292
|
+
"$bbox", "$area", "$length", "$centroid"
|
|
255
293
|
]
|
|
256
294
|
},
|
|
257
295
|
"additionalProperties": { "$ref": "#/$defs/expression" }
|
|
@@ -266,7 +304,9 @@
|
|
|
266
304
|
"$eq", "$ne", "$lt", "$le", "$gt", "$ge",
|
|
267
305
|
"$add", "$sub", "$mul", "$div", "$idiv", "$mod",
|
|
268
306
|
"$contains", "$starts-with", "$ends-with", "$match", "$search",
|
|
269
|
-
"$index-of", "$range", "$get",
|
|
307
|
+
"$index-of", "$range", "$get",
|
|
308
|
+
"$start-of", "$end-of", "$date-format",
|
|
309
|
+
"$distance", "$within", "$bbox-intersects", "$default"
|
|
270
310
|
]
|
|
271
311
|
},
|
|
272
312
|
"additionalProperties": {
|
|
@@ -304,12 +344,12 @@
|
|
|
304
344
|
}
|
|
305
345
|
},
|
|
306
346
|
"conditionalArityOperatorPhrase": {
|
|
307
|
-
"description": "Operators taking two or three operand expressions: $if [cond, then, else?], $substring [str, start, len?], $subsequence [seq, start, len?].",
|
|
347
|
+
"description": "Operators taking two or three operand expressions: $if [cond, then, else?], $substring [str, start, len?], $subsequence [seq, start, len?], $date-add / $date-sub [date, duration] or [date, amount, unit].",
|
|
308
348
|
"type": "object",
|
|
309
349
|
"minProperties": 1,
|
|
310
350
|
"maxProperties": 1,
|
|
311
351
|
"propertyNames": {
|
|
312
|
-
"enum": ["$if", "$substring", "$subsequence"]
|
|
352
|
+
"enum": ["$if", "$substring", "$subsequence", "$date-add", "$date-sub"]
|
|
313
353
|
},
|
|
314
354
|
"additionalProperties": {
|
|
315
355
|
"type": "array",
|
|
@@ -318,6 +358,20 @@
|
|
|
318
358
|
"maxItems": 3
|
|
319
359
|
}
|
|
320
360
|
},
|
|
361
|
+
"geohashPhrase": {
|
|
362
|
+
"description": "$geohash [value] or [value, precision]: a GeoJSON value as a base-32 cell string. Precision is an integer from 1 to 12 (default 9); a value that is not a bare position is represented by its centroid.",
|
|
363
|
+
"type": "object",
|
|
364
|
+
"properties": {
|
|
365
|
+
"$geohash": {
|
|
366
|
+
"type": "array",
|
|
367
|
+
"items": { "$ref": "#/$defs/expression" },
|
|
368
|
+
"minItems": 1,
|
|
369
|
+
"maxItems": 2
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
"required": ["$geohash"],
|
|
373
|
+
"additionalProperties": false
|
|
374
|
+
},
|
|
321
375
|
"stringJoinPhrase": {
|
|
322
376
|
"description": "$string-join [seq, separator?]: one or two operand expressions.",
|
|
323
377
|
"type": "object",
|
|
@@ -332,6 +386,19 @@
|
|
|
332
386
|
"required": ["$string-join"],
|
|
333
387
|
"additionalProperties": false
|
|
334
388
|
},
|
|
389
|
+
"ternaryOperatorPhrase": {
|
|
390
|
+
"description": "Operators whose value is an array of exactly three operand expressions: $date-diff [from, to, unit].",
|
|
391
|
+
"type": "object",
|
|
392
|
+
"minProperties": 1,
|
|
393
|
+
"maxProperties": 1,
|
|
394
|
+
"propertyNames": { "enum": ["$date-diff"] },
|
|
395
|
+
"additionalProperties": {
|
|
396
|
+
"type": "array",
|
|
397
|
+
"items": { "$ref": "#/$defs/expression" },
|
|
398
|
+
"minItems": 3,
|
|
399
|
+
"maxItems": 3
|
|
400
|
+
}
|
|
401
|
+
},
|
|
335
402
|
"replacePhrase": {
|
|
336
403
|
"description": "$replace [input, pattern, replacement]: exactly three operand expressions (I-Regexp has no flags argument, D5).",
|
|
337
404
|
"type": "object",
|
package/src/basic.js
CHANGED
|
@@ -199,7 +199,7 @@ export function isValidRelativeJSONPointer(str) {
|
|
|
199
199
|
// \.[a-zA-Z_][a-zA-Z0-9_]*| - dot notation (e.g., $.store)
|
|
200
200
|
// \.[*]| - dot wildcard (e.g., $.*)
|
|
201
201
|
// \[\s*(?:'[^']*'|"[^"]*"|\d+|\*|\?[^\]]*|\d*:\d*(?::\d*)?)\s*\] - bracket notation
|
|
202
|
-
const CONST_REGEXP_JSONPATH = /^(\$|@)(?:\.\.[a-zA-Z_][a-zA-Z0-9_]*|\.\.|\.[a-zA-Z_][a-zA-Z0-9_]*|\.[*]|\[\s*(?:'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|\d+|\*|\?[^\]]*|\d*:\d*(?::\d*)?|\d+(?:\s*,\s*\d+)*|'(?:[^'\\]|\\.)*'(?:\s*,\s*'(?:[^'\\]|\\.)*')
|
|
202
|
+
const CONST_REGEXP_JSONPATH = /^(\$|@)(?:\.\.[a-zA-Z_][a-zA-Z0-9_]*|\.\.|\.[a-zA-Z_][a-zA-Z0-9_]*|\.[*]|\[\s*(?:'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|\d+|\*|\?[^\]]*|\d*:\d*(?::\d*)?|\d+(?:\s*,\s*\d+)*|'(?:[^'\\]|\\.)*'(?:\s*,\s*'(?:[^'\\]|\\.)*')*|"(?:[^"\\]|\\.)*"(?:\s*,\s*"(?:[^"\\]|\\.)*")*)\s*\])*$/;
|
|
203
203
|
|
|
204
204
|
/**
|
|
205
205
|
* Validates a JSONPath expression string per RFC 9535.
|
package/src/canonical.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
//#region Canonical JSON (RFC 8785 / JCS)
|
|
2
|
+
// JSON Canonicalization Scheme
|
|
3
|
+
// https://www.rfc-editor.org/rfc/rfc8785.html
|
|
4
|
+
//
|
|
5
|
+
// Deterministic serialization for hashing and signing: two structurally
|
|
6
|
+
// equal JSON values always produce byte-identical output, so a hash or
|
|
7
|
+
// signature over the text is a hash or signature over the value.
|
|
8
|
+
//
|
|
9
|
+
// The package already had two stable serializers before this one, and
|
|
10
|
+
// neither is JCS. Both stay as they are, because both are load-bearing
|
|
11
|
+
// where they live:
|
|
12
|
+
//
|
|
13
|
+
// - `stableKeyString` (query/runtime.js) is a grouping/ordering KEY.
|
|
14
|
+
// It emits `NaN` and `Infinity` by name so that NaN groups with NaN
|
|
15
|
+
// per the XQuery grouping rule - which JCS forbids outright, since
|
|
16
|
+
// neither is a JSON number.
|
|
17
|
+
// - `stableStringify` (@jarenjs/core/object) is a cache/memo
|
|
18
|
+
// fingerprint. It follows `JSON.stringify` conventions: `NaN`
|
|
19
|
+
// becomes `null`, `undefined` members are dropped.
|
|
20
|
+
//
|
|
21
|
+
// Silently dropping or coercing a value is exactly right for a memo key
|
|
22
|
+
// and exactly wrong for a signature, so this module rejects instead:
|
|
23
|
+
// every input that is not JSON data is an error, never a guess.
|
|
24
|
+
|
|
25
|
+
import { encodeJSONPointerSegment } from './pointer.js';
|
|
26
|
+
|
|
27
|
+
const hasOwn = Object.hasOwn;
|
|
28
|
+
|
|
29
|
+
// In a Unicode-mode pattern a well-formed surrogate pair is one code
|
|
30
|
+
// point, so this matches only UNPAIRED surrogates.
|
|
31
|
+
const RE_LONE_SURROGATE = /\p{Surrogate}/u;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Error thrown when a value cannot be canonicalized because it is not
|
|
35
|
+
* JSON data (RFC 8785 section 3.2.1). `dataPath` is an RFC 6901 JSON
|
|
36
|
+
* Pointer to the offending value, so a rejected document says which
|
|
37
|
+
* member was at fault rather than only that one was.
|
|
38
|
+
*
|
|
39
|
+
* Extends `TypeError`: passing a non-JSON value to a canonicalizer is a
|
|
40
|
+
* programming error, the same class `JSON.stringify` raises for a
|
|
41
|
+
* BigInt or a cycle.
|
|
42
|
+
*/
|
|
43
|
+
export class JsonCanonicalizeError extends TypeError {
|
|
44
|
+
constructor(message, dataPath) {
|
|
45
|
+
super(`${message} at ${dataPath === '' ? 'the document root' : dataPath}`);
|
|
46
|
+
this.name = 'JsonCanonicalizeError';
|
|
47
|
+
this.dataPath = dataPath;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fail(message, dataPath) {
|
|
52
|
+
throw new JsonCanonicalizeError(message, dataPath);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// RFC 6901 escaping for the diagnostic pointer only.
|
|
56
|
+
function appendPath(path, token) {
|
|
57
|
+
return path + '/' + encodeJSONPointerSegment(token);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function serializeString(value, path) {
|
|
61
|
+
if (RE_LONE_SURROGATE.test(value))
|
|
62
|
+
fail('a string with an unpaired surrogate is not valid Unicode', path);
|
|
63
|
+
// Section 3.2.2.2 is the JSON string escape discipline with the
|
|
64
|
+
// shortest possible escapes and no escaping of non-ASCII - which is
|
|
65
|
+
// precisely what ECMAScript's JSON.stringify emits for a string.
|
|
66
|
+
return JSON.stringify(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function serializeNumber(value, path) {
|
|
70
|
+
if (!Number.isFinite(value))
|
|
71
|
+
fail(`${value === value ? String(value) : 'NaN'} is not a JSON number`, path);
|
|
72
|
+
// Section 3.2.2.3 mandates ECMAScript's Number::toString, which is
|
|
73
|
+
// what String() is - including `-0` serializing as `0`.
|
|
74
|
+
return String(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function serializeValue(value, path, stack) {
|
|
78
|
+
if (value === null)
|
|
79
|
+
return 'null';
|
|
80
|
+
|
|
81
|
+
switch (typeof value) {
|
|
82
|
+
case 'boolean':
|
|
83
|
+
return value ? 'true' : 'false';
|
|
84
|
+
case 'number':
|
|
85
|
+
return serializeNumber(value, path);
|
|
86
|
+
case 'string':
|
|
87
|
+
return serializeString(value, path);
|
|
88
|
+
case 'object':
|
|
89
|
+
break;
|
|
90
|
+
case 'undefined':
|
|
91
|
+
return fail('undefined is not a JSON value', path);
|
|
92
|
+
case 'bigint':
|
|
93
|
+
return fail('a BigInt is not a JSON number', path);
|
|
94
|
+
default: // 'function', 'symbol'
|
|
95
|
+
return fail(`a ${typeof value} is not a JSON value`, path);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (stack.has(value))
|
|
99
|
+
fail('a circular reference cannot be canonicalized', path);
|
|
100
|
+
stack.add(value);
|
|
101
|
+
|
|
102
|
+
let out;
|
|
103
|
+
if (Array.isArray(value)) {
|
|
104
|
+
out = '[';
|
|
105
|
+
for (let i = 0; i < value.length; i++) {
|
|
106
|
+
if (i > 0)
|
|
107
|
+
out += ',';
|
|
108
|
+
// an array hole reads as undefined, which is not a JSON value
|
|
109
|
+
out += serializeValue(value[i], path + '/' + i, stack);
|
|
110
|
+
}
|
|
111
|
+
out += ']';
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// A signing primitive must not guess. A class instance would walk
|
|
115
|
+
// as its own enumerable fields (a Date as `{}`, a Map as `{}`),
|
|
116
|
+
// which is a silently wrong signature rather than a loud error, so
|
|
117
|
+
// only plain objects are JSON objects here. `toJSON` is
|
|
118
|
+
// deliberately not consulted for the same reason: the value that
|
|
119
|
+
// gets signed is the value that was passed in.
|
|
120
|
+
const proto = Object.getPrototypeOf(value);
|
|
121
|
+
if (proto !== null && proto !== Object.prototype)
|
|
122
|
+
fail(`a ${value.constructor?.name ?? 'non-plain'} instance is not a JSON object`, path);
|
|
123
|
+
// Section 3.2.3: members sorted by the UTF-16 code units of their
|
|
124
|
+
// names, which is what the default string sort compares.
|
|
125
|
+
const keys = Object.keys(value).sort();
|
|
126
|
+
out = '{';
|
|
127
|
+
for (let i = 0; i < keys.length; i++) {
|
|
128
|
+
const key = keys[i];
|
|
129
|
+
/* c8 ignore next 2 -- Object.keys only yields own keys */
|
|
130
|
+
if (!hasOwn(value, key))
|
|
131
|
+
continue;
|
|
132
|
+
if (i > 0)
|
|
133
|
+
out += ',';
|
|
134
|
+
out += serializeString(key, path) + ':'
|
|
135
|
+
+ serializeValue(value[key], appendPath(path, key), stack);
|
|
136
|
+
}
|
|
137
|
+
out += '}';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
stack.delete(value);
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Serialize a JSON value to its canonical form (RFC 8785 / JCS): object
|
|
146
|
+
* members sorted by the UTF-16 code units of their names, numbers in
|
|
147
|
+
* the ECMAScript `Number::toString` form, strings with the shortest
|
|
148
|
+
* JSON escapes and no escaping of non-ASCII, and no insignificant
|
|
149
|
+
* whitespace. Two structurally equal values always produce identical
|
|
150
|
+
* text, which is what makes the result safe to hash or sign.
|
|
151
|
+
*
|
|
152
|
+
* Unlike `JSON.stringify`, nothing is dropped or coerced: `undefined`,
|
|
153
|
+
* a function, a symbol, a BigInt, `NaN`, `Infinity`, an unpaired
|
|
154
|
+
* surrogate, a circular reference and a class instance are all errors,
|
|
155
|
+
* because a canonicalizer that quietly rewrote its input would produce
|
|
156
|
+
* a signature over a document nobody sent.
|
|
157
|
+
*
|
|
158
|
+
* @param {any} value - The JSON value to canonicalize
|
|
159
|
+
* @returns {string} The canonical JSON text
|
|
160
|
+
* @throws {JsonCanonicalizeError} When the value is not JSON data
|
|
161
|
+
* @example
|
|
162
|
+
* canonicalizeJson({ b: 1, a: [2, 3] }); // '{"a":[2,3],"b":1}'
|
|
163
|
+
* canonicalizeJson({ 'ä': 1, 'a': 2 }); // '{"a":2,"ä":1}'
|
|
164
|
+
* canonicalizeJson(1e21); // '1e+21'
|
|
165
|
+
*/
|
|
166
|
+
export function canonicalizeJson(value) {
|
|
167
|
+
return serializeValue(value, '', new Set());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
//#endregion
|
package/src/cow.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
//#region copy-on-write machinery (package-internal)
|
|
2
|
+
// The shared copy-on-write core of the JSON Patch engine (patch.js) and
|
|
3
|
+
// the standalone write operations (write.js). Like segments.js, this
|
|
4
|
+
// module is package-internal and is deliberately not listed in the
|
|
5
|
+
// package exports. The value-level primitives it builds on
|
|
6
|
+
// (isJsonContainer, shallowCloneJson, cloneJson) are pure JSON helpers
|
|
7
|
+
// and live in @jarenjs/core/object.
|
|
8
|
+
//
|
|
9
|
+
// An application of writes carries a state `{ root, owned }` where
|
|
10
|
+
// `owned` is the set of nodes this application created and may mutate
|
|
11
|
+
// freely (`null` = in-place mode: every node is owned). The first write
|
|
12
|
+
// along a path shallow-clones the spine from the root down; later writes
|
|
13
|
+
// find the spine in the owned set and mutate the clones in place. The
|
|
14
|
+
// input document is never touched.
|
|
15
|
+
//
|
|
16
|
+
// Step encoding shared by the walk/read helpers: a location is a pair of
|
|
17
|
+
// parallel arrays `names`/`indexes`.
|
|
18
|
+
// - RFC 6901 form (patch.js, pointer targets): `names[i]` is the
|
|
19
|
+
// decoded token, `indexes[i]` its pre-scanned array-index form
|
|
20
|
+
// (-1 = not a valid index). One token, two forms.
|
|
21
|
+
// - Typed form (JSONPath-derived targets): a name selector stores
|
|
22
|
+
// `names[i] = name, indexes[i] = -1`; an index selector stores
|
|
23
|
+
// `names[i] = null, indexes[i] = index` (negative = from the end).
|
|
24
|
+
// `readSteps` resolves either form; the callers own their walk loops so
|
|
25
|
+
// each module raises its own error types.
|
|
26
|
+
|
|
27
|
+
import { isJsonContainer, shallowCloneJson } from '@jarenjs/core/object';
|
|
28
|
+
|
|
29
|
+
const hasOwn = Object.hasOwn;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The mutable state of one write application. `owned` is the set of
|
|
33
|
+
* nodes this application created and may mutate freely; `null` means
|
|
34
|
+
* in-place mode (every node is owned).
|
|
35
|
+
*/
|
|
36
|
+
export function makeState(root, owned) {
|
|
37
|
+
return { root, owned };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Ensure the root is owned before the first write into it. */
|
|
41
|
+
export function ownedRoot(state) {
|
|
42
|
+
const root = state.root;
|
|
43
|
+
const owned = state.owned;
|
|
44
|
+
if (owned === null || !isJsonContainer(root) || owned.has(root))
|
|
45
|
+
return root;
|
|
46
|
+
const clone = shallowCloneJson(root);
|
|
47
|
+
owned.add(clone);
|
|
48
|
+
state.root = clone;
|
|
49
|
+
return clone;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Return an owned version of `child`, writing the clone back into the
|
|
54
|
+
* (already owned) parent slot when one is taken.
|
|
55
|
+
*/
|
|
56
|
+
export function ownedChild(state, parent, child, key) {
|
|
57
|
+
const owned = state.owned;
|
|
58
|
+
if (owned === null || !isJsonContainer(child) || owned.has(child))
|
|
59
|
+
return child;
|
|
60
|
+
const clone = shallowCloneJson(child);
|
|
61
|
+
owned.add(clone);
|
|
62
|
+
// the slot was just read through hasOwn/index, so plain assignment
|
|
63
|
+
// never reaches a prototype '__proto__' setter
|
|
64
|
+
parent[key] = clone;
|
|
65
|
+
return clone;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The concrete array index a step addresses in `parent`, or -1 when the
|
|
70
|
+
* step cannot address an array element. Typed index steps resolve
|
|
71
|
+
* negative indexes from the end; the RFC 6901 form never does.
|
|
72
|
+
*/
|
|
73
|
+
export function stepArrayIndex(parent, name, index) {
|
|
74
|
+
if (name === null)
|
|
75
|
+
return index < 0 ? parent.length + index : index;
|
|
76
|
+
return index;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Read the location `steps[0..len)` in `root` without cloning anything.
|
|
81
|
+
* Returns the shared NOTHING-style `miss` sentinel value passed in when
|
|
82
|
+
* the location does not exist (callers pick their own sentinel).
|
|
83
|
+
*/
|
|
84
|
+
export function readSteps(root, names, indexes, len, miss) {
|
|
85
|
+
let v = root;
|
|
86
|
+
for (let i = 0; i < len; i++) {
|
|
87
|
+
if (Array.isArray(v)) {
|
|
88
|
+
const idx = stepArrayIndex(v, names[i], indexes[i]);
|
|
89
|
+
if (idx < 0 || idx >= v.length)
|
|
90
|
+
return miss;
|
|
91
|
+
v = v[idx];
|
|
92
|
+
}
|
|
93
|
+
else if (typeof v === 'object' && v !== null) {
|
|
94
|
+
const name = names[i];
|
|
95
|
+
if (name === null || !hasOwn(v, name))
|
|
96
|
+
return miss;
|
|
97
|
+
v = v[name];
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
return miss;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return v;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
//#endregion
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//#region Jaren shared error bases
|
|
2
|
+
// Shared constructors for the coded error classes of the json package.
|
|
3
|
+
// Each engine keeps its own exported class (the `name` and `message`
|
|
4
|
+
// shapes are public API); these bases only centralize the constructor
|
|
5
|
+
// bodies that were byte-identical across engines. The `name` is passed
|
|
6
|
+
// as a string literal because the bundle is minified and a mangled
|
|
7
|
+
// class name must not leak into `error.name`.
|
|
8
|
+
|
|
9
|
+
import { CodedError } from '@jarenjs/core/errors';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Base for errors that carry a stable `code` and a `docPath`, an RFC
|
|
13
|
+
* 6901 JSON Pointer into the offending document. A wrapped error is
|
|
14
|
+
* exposed through the native `cause` option when one is given. This is
|
|
15
|
+
* the positional adapter over `@jarenjs/core`'s {@link CodedError}: a
|
|
16
|
+
* trailing `undefined` cause means "no cause" (no own property), per
|
|
17
|
+
* the base's contract.
|
|
18
|
+
*/
|
|
19
|
+
export class CodedDocPathError extends CodedError {
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} name - The public class name for `error.name`
|
|
22
|
+
* @param {string} code - Stable diagnosis code
|
|
23
|
+
* @param {string} reason - What is wrong (bare; the message is composed)
|
|
24
|
+
* @param {string} docPath - JSON Pointer into the offending document
|
|
25
|
+
* @param {unknown} [cause] - Wrapped error, when there is one
|
|
26
|
+
*/
|
|
27
|
+
constructor(name, code, reason, docPath, cause = undefined) {
|
|
28
|
+
super(name, code, reason, docPath,
|
|
29
|
+
cause === undefined ? undefined : { cause });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Base for syntax errors over a source string: `source` and `position`
|
|
35
|
+
* locate the offending token, and the message is prefixed with a
|
|
36
|
+
* human-readable language label. Deliberately NOT a {@link CodedError}:
|
|
37
|
+
* this family carries no stable code and locates by source offset, not
|
|
38
|
+
* by document pointer.
|
|
39
|
+
*/
|
|
40
|
+
export class LabeledSyntaxError extends SyntaxError {
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} name - The public class name for `error.name`
|
|
43
|
+
* @param {string} label - Language label for the message prefix
|
|
44
|
+
* @param {string} message - What is wrong
|
|
45
|
+
* @param {string} source - The offending source text
|
|
46
|
+
* @param {number} position - 0-based index of the offending token
|
|
47
|
+
*/
|
|
48
|
+
constructor(name, label, message, source, position) {
|
|
49
|
+
super(`Invalid ${label}: ${message} at position ${position} in '${source}'`);
|
|
50
|
+
this.name = name;
|
|
51
|
+
this.source = source;
|
|
52
|
+
this.position = position;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build a `fail(code, message, docPath)` throw-adapter for a coded
|
|
58
|
+
* error class; compilers use it to keep rejection one-liners readable.
|
|
59
|
+
* @param {new (code: string, message: string, docPath: string) => Error} ErrorClass
|
|
60
|
+
* @returns {(code: string, message: string, docPath: string) => never}
|
|
61
|
+
*/
|
|
62
|
+
export function failerFor(ErrorClass) {
|
|
63
|
+
return function fail(code, message, docPath) {
|
|
64
|
+
throw new ErrorClass(code, message, docPath);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
//#endregion
|