@jarenjs/json 0.9.2
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 +175 -0
- package/LICENSE +21 -0
- package/README.md +471 -0
- package/dist/types/basic.d.ts +32 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/jslt/dispatch.d.ts +11 -0
- package/dist/types/jslt/errors.d.ts +18 -0
- package/dist/types/jslt/index.d.ts +53 -0
- package/dist/types/jslt/stylesheet.d.ts +8 -0
- package/dist/types/jtlt/desugar.d.ts +19 -0
- package/dist/types/jtlt/errors.d.ts +18 -0
- package/dist/types/jtlt/index.d.ts +57 -0
- package/dist/types/jtlt/template.d.ts +8 -0
- package/dist/types/jtlt/writer.d.ts +6 -0
- package/dist/types/path.d.ts +235 -0
- package/dist/types/pointer.d.ts +114 -0
- package/dist/types/query/compile.d.ts +21 -0
- package/dist/types/query/errors.d.ts +18 -0
- package/dist/types/query/index.d.ts +70 -0
- package/dist/types/query/normalize.d.ts +68 -0
- package/dist/types/query/operators.d.ts +424 -0
- package/dist/types/query/runtime.d.ts +93 -0
- package/dist/types/segments.d.ts +62 -0
- package/dist/types/xquery/index.d.ts +19 -0
- package/dist/types/xquery/parse.d.ts +20 -0
- package/docs/JSLT-FORMAT.md +861 -0
- package/docs/JSLT-PRELUDE.md +159 -0
- package/docs/JTLT-FORMAT.md +659 -0
- package/docs/QUERY-FORMAT.md +1221 -0
- package/docs/XQUERY-FRONTEND.md +321 -0
- package/package.json +81 -0
- package/schemas/jaren-jslt.draft-07.schema.json +776 -0
- package/schemas/jaren-jslt.schema.json +776 -0
- package/schemas/jaren-query.draft-07.schema.json +613 -0
- package/schemas/jaren-query.schema.json +375 -0
- package/src/basic.js +300 -0
- package/src/index.js +4 -0
- package/src/jslt/dispatch.js +934 -0
- package/src/jslt/errors.js +34 -0
- package/src/jslt/index.js +121 -0
- package/src/jslt/stylesheet.js +234 -0
- package/src/jtlt/desugar.js +231 -0
- package/src/jtlt/errors.js +34 -0
- package/src/jtlt/index.js +155 -0
- package/src/jtlt/template.js +130 -0
- package/src/jtlt/writer.js +110 -0
- package/src/path.js +977 -0
- package/src/pointer.js +453 -0
- package/src/query/compile.js +817 -0
- package/src/query/errors.js +33 -0
- package/src/query/index.js +150 -0
- package/src/query/normalize.js +1047 -0
- package/src/query/operators.js +1253 -0
- package/src/query/runtime.js +233 -0
- package/src/segments.js +627 -0
- package/src/xquery/index.js +35 -0
- package/src/xquery/parse.js +1647 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://jarenjs.dev/schemas/jaren-query/0.1",
|
|
4
|
+
"title": "Jaren JSON Query format 0.1",
|
|
5
|
+
"description": "Structural grammar of Jaren JSON Query documents (see packages/json/docs/QUERY-FORMAT.md). This schema is structural validation only: the compiler remains authoritative for semantic rules it cannot express - FLWOR clause keys apply in the fixed semantic order $for, $let, $as, $where, $groupby, $orderby, $count, $return regardless of JSON key order; variable scoping, duplicate-binding detection, external-parameter collection and $as name binding are compiler rules (JQ0005/JQ0007); variable-rooted path strings are only head-checked here (full RFC 9535 segment grammar is JQ0004); embedded JSON Schema literals are taken verbatim and never meta-validated here (the type-test compiler is authoritative, JQ0008/JQ0009); per-argument runtime typing is JQ2xxx. Authored in a draft-neutral keyword subset; the draft-07 twin is a mechanical derivation.",
|
|
6
|
+
"allOf": [
|
|
7
|
+
{ "$ref": "#/$defs/queryDocument" }
|
|
8
|
+
],
|
|
9
|
+
"$defs": {
|
|
10
|
+
"queryDocument": {
|
|
11
|
+
"description": "A query document is either the version envelope or a bare expression (a bare RFC 9535 JSONPath string is the degenerate query).",
|
|
12
|
+
"oneOf": [
|
|
13
|
+
{ "$ref": "#/$defs/versionEnvelope" },
|
|
14
|
+
{ "$ref": "#/$defs/expression" }
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"versionEnvelope": {
|
|
18
|
+
"description": "Top-level version envelope. Only recognized at the top level of a query document.",
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": {
|
|
21
|
+
"$query": { "const": "0.1" },
|
|
22
|
+
"$expr": { "$ref": "#/$defs/expression" }
|
|
23
|
+
},
|
|
24
|
+
"required": ["$query", "$expr"],
|
|
25
|
+
"additionalProperties": false
|
|
26
|
+
},
|
|
27
|
+
"expression": {
|
|
28
|
+
"description": "Any expression per the encoding rules: scalars are literals, strings split on the leading '$', arrays are array constructors, objects partition into map constructors (no $-prefixed key) and operator phrases (all keys $-prefixed).",
|
|
29
|
+
"oneOf": [
|
|
30
|
+
{ "$ref": "#/$defs/scalarLiteral" },
|
|
31
|
+
{ "$ref": "#/$defs/stringExpression" },
|
|
32
|
+
{ "$ref": "#/$defs/arrayConstructor" },
|
|
33
|
+
{ "$ref": "#/$defs/objectExpression" }
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"scalarLiteral": {
|
|
37
|
+
"description": "Numbers, booleans and null are literal items.",
|
|
38
|
+
"type": ["null", "boolean", "number"]
|
|
39
|
+
},
|
|
40
|
+
"stringExpression": {
|
|
41
|
+
"description": "Rule 2: a string starting with '$' is a query expression ('$$' escapes a literal); any other string is a literal.",
|
|
42
|
+
"type": "string",
|
|
43
|
+
"anyOf": [
|
|
44
|
+
{ "$ref": "#/$defs/literalString" },
|
|
45
|
+
{ "$ref": "#/$defs/escapedStringLiteral" },
|
|
46
|
+
{ "$ref": "#/$defs/absolutePathString" },
|
|
47
|
+
{ "$ref": "#/$defs/variablePathString" }
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"literalString": {
|
|
51
|
+
"description": "A string that does not start with '$' denotes itself.",
|
|
52
|
+
"type": "string",
|
|
53
|
+
"pattern": "^([^$]|$)"
|
|
54
|
+
},
|
|
55
|
+
"escapedStringLiteral": {
|
|
56
|
+
"description": "A string starting with '$$' denotes the literal string with one leading '$' dropped.",
|
|
57
|
+
"type": "string",
|
|
58
|
+
"pattern": "^\\$\\$"
|
|
59
|
+
},
|
|
60
|
+
"absolutePathString": {
|
|
61
|
+
"description": "An absolute RFC 9535 JSONPath query rooted at the input document: '$' alone or starting '$.', '$[' or '$..'. The json-path format asserts the full RFC 9535 grammar where format assertion is enabled (format is annotation-only by default from draft 2020-12).",
|
|
62
|
+
"type": "string",
|
|
63
|
+
"pattern": "^\\$($|[.\\[])",
|
|
64
|
+
"format": "json-path"
|
|
65
|
+
},
|
|
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. Only the head is pattern-checked; the full segment grammar is enforced by the compiler (JQ0004).",
|
|
68
|
+
"type": "string",
|
|
69
|
+
"pattern": "^\\$[A-Za-z_][A-Za-z0-9_]*($|[.\\[])"
|
|
70
|
+
},
|
|
71
|
+
"arrayConstructor": {
|
|
72
|
+
"description": "An array in expression position constructs an array; element results are concatenated per XQuery sequence flattening.",
|
|
73
|
+
"type": "array",
|
|
74
|
+
"items": { "$ref": "#/$defs/expression" }
|
|
75
|
+
},
|
|
76
|
+
"objectExpression": {
|
|
77
|
+
"description": "Rule 1: objects partition by $-prefixed keys. A mixed object matches neither branch and is invalid (JQ0001).",
|
|
78
|
+
"oneOf": [
|
|
79
|
+
{ "$ref": "#/$defs/mapConstructor" },
|
|
80
|
+
{ "$ref": "#/$defs/constPhrase" },
|
|
81
|
+
{ "$ref": "#/$defs/mapPhrase" },
|
|
82
|
+
{ "$ref": "#/$defs/flworPhrase" },
|
|
83
|
+
{ "$ref": "#/$defs/somePhrase" },
|
|
84
|
+
{ "$ref": "#/$defs/everyPhrase" },
|
|
85
|
+
{ "$ref": "#/$defs/unaryOperatorPhrase" },
|
|
86
|
+
{ "$ref": "#/$defs/binaryOperatorPhrase" },
|
|
87
|
+
{ "$ref": "#/$defs/variadicOperatorPhrase" },
|
|
88
|
+
{ "$ref": "#/$defs/nonEmptyVariadicOperatorPhrase" },
|
|
89
|
+
{ "$ref": "#/$defs/conditionalArityOperatorPhrase" },
|
|
90
|
+
{ "$ref": "#/$defs/stringJoinPhrase" },
|
|
91
|
+
{ "$ref": "#/$defs/replacePhrase" },
|
|
92
|
+
{ "$ref": "#/$defs/schemaOperatorPhrase" }
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
"mapConstructor": {
|
|
96
|
+
"description": "An object with no $-prefixed key: keys are literal member names, values are expressions. Use $map or $const for member names that start with '$'.",
|
|
97
|
+
"type": "object",
|
|
98
|
+
"patternProperties": {
|
|
99
|
+
"^\\$": false
|
|
100
|
+
},
|
|
101
|
+
"additionalProperties": { "$ref": "#/$defs/expression" }
|
|
102
|
+
},
|
|
103
|
+
"constPhrase": {
|
|
104
|
+
"description": "Quote: the value is returned verbatim, nothing inside is evaluated.",
|
|
105
|
+
"type": "object",
|
|
106
|
+
"properties": {
|
|
107
|
+
"$const": true
|
|
108
|
+
},
|
|
109
|
+
"required": ["$const"],
|
|
110
|
+
"additionalProperties": false
|
|
111
|
+
},
|
|
112
|
+
"mapPhrase": {
|
|
113
|
+
"description": "General map constructor for computed keys or keys starting with '$'. Each entry is a [keyExpr, valueExpr] pair; keyExpr must evaluate to a single string at runtime (JQ2004); later pairs win on duplicate keys.",
|
|
114
|
+
"type": "object",
|
|
115
|
+
"properties": {
|
|
116
|
+
"$map": {
|
|
117
|
+
"type": "array",
|
|
118
|
+
"items": { "$ref": "#/$defs/mapEntry" }
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"required": ["$map"],
|
|
122
|
+
"additionalProperties": false
|
|
123
|
+
},
|
|
124
|
+
"mapEntry": {
|
|
125
|
+
"description": "One [keyExpr, valueExpr] pair of a $map phrase.",
|
|
126
|
+
"type": "array",
|
|
127
|
+
"items": { "$ref": "#/$defs/expression" },
|
|
128
|
+
"minItems": 2,
|
|
129
|
+
"maxItems": 2
|
|
130
|
+
},
|
|
131
|
+
"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.",
|
|
133
|
+
"type": "object",
|
|
134
|
+
"properties": {
|
|
135
|
+
"$for": { "$ref": "#/$defs/forBindings" },
|
|
136
|
+
"$let": { "$ref": "#/$defs/bindingMap" },
|
|
137
|
+
"$as": { "$ref": "#/$defs/asClause" },
|
|
138
|
+
"$where": { "$ref": "#/$defs/expression" },
|
|
139
|
+
"$groupby": { "$ref": "#/$defs/bindingMap" },
|
|
140
|
+
"$orderby": { "$ref": "#/$defs/orderBySpec" },
|
|
141
|
+
"$count": { "$ref": "#/$defs/variableName" },
|
|
142
|
+
"$return": { "$ref": "#/$defs/expression" }
|
|
143
|
+
},
|
|
144
|
+
"required": ["$return"],
|
|
145
|
+
"anyOf": [
|
|
146
|
+
{ "required": ["$for"] },
|
|
147
|
+
{ "required": ["$let"] }
|
|
148
|
+
],
|
|
149
|
+
"additionalProperties": false
|
|
150
|
+
},
|
|
151
|
+
"variableName": {
|
|
152
|
+
"type": "string",
|
|
153
|
+
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
|
|
154
|
+
},
|
|
155
|
+
"forBindings": {
|
|
156
|
+
"description": "Iteration bindings: variable name to source expression or extended {$in, $at} binding. Bindings nest left-to-right in document key order and may be correlated; key-order-hostile producers should nest phrases instead.",
|
|
157
|
+
"type": "object",
|
|
158
|
+
"minProperties": 1,
|
|
159
|
+
"propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
|
|
160
|
+
"additionalProperties": { "$ref": "#/$defs/forSource" }
|
|
161
|
+
},
|
|
162
|
+
"forSource": {
|
|
163
|
+
"oneOf": [
|
|
164
|
+
{ "$ref": "#/$defs/expression" },
|
|
165
|
+
{ "$ref": "#/$defs/extendedForBinding" }
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
"extendedForBinding": {
|
|
169
|
+
"description": "Extended $for binding: iterates $in and binds $at to the 0-based (D6) position variable.",
|
|
170
|
+
"type": "object",
|
|
171
|
+
"properties": {
|
|
172
|
+
"$in": { "$ref": "#/$defs/expression" },
|
|
173
|
+
"$at": { "$ref": "#/$defs/variableName" }
|
|
174
|
+
},
|
|
175
|
+
"required": ["$in", "$at"],
|
|
176
|
+
"additionalProperties": false
|
|
177
|
+
},
|
|
178
|
+
"bindingMap": {
|
|
179
|
+
"description": "Variable name to expression map, used by $let, $groupby and quantifier bindings.",
|
|
180
|
+
"type": "object",
|
|
181
|
+
"minProperties": 1,
|
|
182
|
+
"propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
|
|
183
|
+
"additionalProperties": { "$ref": "#/$defs/expression" }
|
|
184
|
+
},
|
|
185
|
+
"orderBySpec": {
|
|
186
|
+
"description": "A single key spec or a major-to-minor list of key specs. An array value is by specification ALWAYS a list of key specs, so the single form excludes arrays; write a single array-constructor key as {\"$key\": [...]}.",
|
|
187
|
+
"oneOf": [
|
|
188
|
+
{ "$ref": "#/$defs/singleOrderByKey" },
|
|
189
|
+
{
|
|
190
|
+
"type": "array",
|
|
191
|
+
"items": { "$ref": "#/$defs/orderByKey" },
|
|
192
|
+
"minItems": 1
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
},
|
|
196
|
+
"singleOrderByKey": {
|
|
197
|
+
"description": "A key spec in non-list position: any non-array expression, or the explicit {$key, $dir, $empty} form.",
|
|
198
|
+
"oneOf": [
|
|
199
|
+
{ "$ref": "#/$defs/scalarLiteral" },
|
|
200
|
+
{ "$ref": "#/$defs/stringExpression" },
|
|
201
|
+
{ "$ref": "#/$defs/objectExpression" },
|
|
202
|
+
{ "$ref": "#/$defs/orderByKeySpec" }
|
|
203
|
+
]
|
|
204
|
+
},
|
|
205
|
+
"orderByKey": {
|
|
206
|
+
"oneOf": [
|
|
207
|
+
{ "$ref": "#/$defs/expression" },
|
|
208
|
+
{ "$ref": "#/$defs/orderByKeySpec" }
|
|
209
|
+
]
|
|
210
|
+
},
|
|
211
|
+
"orderByKeySpec": {
|
|
212
|
+
"description": "Explicit order key: direction defaults to 'asc', empty-sequence placement to 'least'.",
|
|
213
|
+
"type": "object",
|
|
214
|
+
"properties": {
|
|
215
|
+
"$key": { "$ref": "#/$defs/expression" },
|
|
216
|
+
"$dir": { "enum": ["asc", "desc"] },
|
|
217
|
+
"$empty": { "enum": ["least", "greatest"] }
|
|
218
|
+
},
|
|
219
|
+
"required": ["$key"],
|
|
220
|
+
"additionalProperties": false
|
|
221
|
+
},
|
|
222
|
+
"somePhrase": {
|
|
223
|
+
"description": "Existential quantifier: true iff some binding tuple satisfies the EBV of $satisfies; short-circuits.",
|
|
224
|
+
"type": "object",
|
|
225
|
+
"properties": {
|
|
226
|
+
"$some": { "$ref": "#/$defs/bindingMap" },
|
|
227
|
+
"$satisfies": { "$ref": "#/$defs/expression" }
|
|
228
|
+
},
|
|
229
|
+
"required": ["$some", "$satisfies"],
|
|
230
|
+
"additionalProperties": false
|
|
231
|
+
},
|
|
232
|
+
"everyPhrase": {
|
|
233
|
+
"description": "Universal quantifier: true iff every binding tuple satisfies the EBV of $satisfies; short-circuits.",
|
|
234
|
+
"type": "object",
|
|
235
|
+
"properties": {
|
|
236
|
+
"$every": { "$ref": "#/$defs/bindingMap" },
|
|
237
|
+
"$satisfies": { "$ref": "#/$defs/expression" }
|
|
238
|
+
},
|
|
239
|
+
"required": ["$every", "$satisfies"],
|
|
240
|
+
"additionalProperties": false
|
|
241
|
+
},
|
|
242
|
+
"unaryOperatorPhrase": {
|
|
243
|
+
"description": "Operators whose value is a single operand expression.",
|
|
244
|
+
"type": "object",
|
|
245
|
+
"minProperties": 1,
|
|
246
|
+
"maxProperties": 1,
|
|
247
|
+
"propertyNames": {
|
|
248
|
+
"enum": [
|
|
249
|
+
"$exists", "$empty", "$not", "$neg",
|
|
250
|
+
"$count", "$sum", "$avg", "$min", "$max",
|
|
251
|
+
"$distinct", "$reverse", "$sort", "$head", "$tail",
|
|
252
|
+
"$upper", "$lower", "$string-length", "$normalize-space",
|
|
253
|
+
"$string", "$number", "$boolean",
|
|
254
|
+
"$is-string", "$is-number", "$is-boolean", "$is-null", "$is-array", "$is-object"
|
|
255
|
+
]
|
|
256
|
+
},
|
|
257
|
+
"additionalProperties": { "$ref": "#/$defs/expression" }
|
|
258
|
+
},
|
|
259
|
+
"binaryOperatorPhrase": {
|
|
260
|
+
"description": "Operators whose value is an array of exactly two operand expressions.",
|
|
261
|
+
"type": "object",
|
|
262
|
+
"minProperties": 1,
|
|
263
|
+
"maxProperties": 1,
|
|
264
|
+
"propertyNames": {
|
|
265
|
+
"enum": [
|
|
266
|
+
"$eq", "$ne", "$lt", "$le", "$gt", "$ge",
|
|
267
|
+
"$add", "$sub", "$mul", "$div", "$idiv", "$mod",
|
|
268
|
+
"$contains", "$starts-with", "$ends-with", "$match", "$search",
|
|
269
|
+
"$index-of", "$range", "$get", "$default"
|
|
270
|
+
]
|
|
271
|
+
},
|
|
272
|
+
"additionalProperties": {
|
|
273
|
+
"type": "array",
|
|
274
|
+
"items": { "$ref": "#/$defs/expression" },
|
|
275
|
+
"minItems": 2,
|
|
276
|
+
"maxItems": 2
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
"variadicOperatorPhrase": {
|
|
280
|
+
"description": "Variadic operators accepting zero or more operand expressions.",
|
|
281
|
+
"type": "object",
|
|
282
|
+
"minProperties": 1,
|
|
283
|
+
"maxProperties": 1,
|
|
284
|
+
"propertyNames": {
|
|
285
|
+
"enum": ["$seq", "$concat"]
|
|
286
|
+
},
|
|
287
|
+
"additionalProperties": {
|
|
288
|
+
"type": "array",
|
|
289
|
+
"items": { "$ref": "#/$defs/expression" }
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
"nonEmptyVariadicOperatorPhrase": {
|
|
293
|
+
"description": "Variadic operators requiring at least one operand expression.",
|
|
294
|
+
"type": "object",
|
|
295
|
+
"minProperties": 1,
|
|
296
|
+
"maxProperties": 1,
|
|
297
|
+
"propertyNames": {
|
|
298
|
+
"enum": ["$and", "$or", "$coalesce"]
|
|
299
|
+
},
|
|
300
|
+
"additionalProperties": {
|
|
301
|
+
"type": "array",
|
|
302
|
+
"items": { "$ref": "#/$defs/expression" },
|
|
303
|
+
"minItems": 1
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
"conditionalArityOperatorPhrase": {
|
|
307
|
+
"description": "Operators taking two or three operand expressions: $if [cond, then, else?], $substring [str, start, len?], $subsequence [seq, start, len?].",
|
|
308
|
+
"type": "object",
|
|
309
|
+
"minProperties": 1,
|
|
310
|
+
"maxProperties": 1,
|
|
311
|
+
"propertyNames": {
|
|
312
|
+
"enum": ["$if", "$substring", "$subsequence"]
|
|
313
|
+
},
|
|
314
|
+
"additionalProperties": {
|
|
315
|
+
"type": "array",
|
|
316
|
+
"items": { "$ref": "#/$defs/expression" },
|
|
317
|
+
"minItems": 2,
|
|
318
|
+
"maxItems": 3
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
"stringJoinPhrase": {
|
|
322
|
+
"description": "$string-join [seq, separator?]: one or two operand expressions.",
|
|
323
|
+
"type": "object",
|
|
324
|
+
"properties": {
|
|
325
|
+
"$string-join": {
|
|
326
|
+
"type": "array",
|
|
327
|
+
"items": { "$ref": "#/$defs/expression" },
|
|
328
|
+
"minItems": 1,
|
|
329
|
+
"maxItems": 2
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
"required": ["$string-join"],
|
|
333
|
+
"additionalProperties": false
|
|
334
|
+
},
|
|
335
|
+
"replacePhrase": {
|
|
336
|
+
"description": "$replace [input, pattern, replacement]: exactly three operand expressions (I-Regexp has no flags argument, D5).",
|
|
337
|
+
"type": "object",
|
|
338
|
+
"properties": {
|
|
339
|
+
"$replace": {
|
|
340
|
+
"type": "array",
|
|
341
|
+
"items": { "$ref": "#/$defs/expression" },
|
|
342
|
+
"minItems": 3,
|
|
343
|
+
"maxItems": 3
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
"required": ["$replace"],
|
|
347
|
+
"additionalProperties": false
|
|
348
|
+
},
|
|
349
|
+
"schemaLiteral": {
|
|
350
|
+
"description": "A JSON Schema literal, taken verbatim - never interpreted as a query expression, so JSON Schema's $-prefixed keywords ($ref, $defs, ...) do not collide with Rule 1. Deliberately unconstrained (a true schema is draft-neutral by definition): embedded schemas are not meta-validated by this artifact; the consumer's type-test compiler is authoritative (JQ0009)."
|
|
351
|
+
},
|
|
352
|
+
"schemaOperatorPhrase": {
|
|
353
|
+
"description": "Schema operators $valid/$assert: [expr, schema] - exactly two elements, an operand expression and a verbatim JSON Schema literal. Tuple validation is outside the draft-neutral subset, so both positions validate uniformly as schema literals here; the operand position is normalized as an ordinary expression by the compiler (JQ0xxx).",
|
|
354
|
+
"type": "object",
|
|
355
|
+
"minProperties": 1,
|
|
356
|
+
"maxProperties": 1,
|
|
357
|
+
"propertyNames": {
|
|
358
|
+
"enum": ["$valid", "$assert"]
|
|
359
|
+
},
|
|
360
|
+
"additionalProperties": {
|
|
361
|
+
"type": "array",
|
|
362
|
+
"items": { "$ref": "#/$defs/schemaLiteral" },
|
|
363
|
+
"minItems": 2,
|
|
364
|
+
"maxItems": 2
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
"asClause": {
|
|
368
|
+
"description": "The $as clause: variable-name to JSON Schema literal members, applied per tuple after $for/$let binding and before $where. Each name must be bound by the same phrase's $for (including $at names) or $let - a compiler rule (JQ0005); each binding validates per item at runtime (JQ2008).",
|
|
369
|
+
"type": "object",
|
|
370
|
+
"minProperties": 1,
|
|
371
|
+
"propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
|
|
372
|
+
"additionalProperties": { "$ref": "#/$defs/schemaLiteral" }
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
package/src/basic.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CC_SLASH,
|
|
3
|
+
CC_HASH,
|
|
4
|
+
CC_PERCENT,
|
|
5
|
+
CC_TILDE,
|
|
6
|
+
CC_0,
|
|
7
|
+
CC_1,
|
|
8
|
+
isDigitCode,
|
|
9
|
+
isHexDigitCode,
|
|
10
|
+
} from '@jarenjs/core/scan';
|
|
11
|
+
|
|
12
|
+
//#region JSON validation
|
|
13
|
+
// JSON whitespace characters: space, tab, newline, carriage return
|
|
14
|
+
const JSON_WHITESPACE = new Set([0x20, 0x09, 0x0A, 0x0D]);
|
|
15
|
+
|
|
16
|
+
export function isValidJSONCheap(data) {
|
|
17
|
+
const len = data.length;
|
|
18
|
+
// Too short to be valid JSON (minimum is 2 for {} or [])
|
|
19
|
+
if (len < 2) return true; // Definitely NOT valid JSON
|
|
20
|
+
|
|
21
|
+
// Check first char
|
|
22
|
+
const first = data.charCodeAt(0);
|
|
23
|
+
|
|
24
|
+
// Objects and arrays - check matching brackets
|
|
25
|
+
if (first === 0x7B || first === 0x5B) { // '{' or '['
|
|
26
|
+
// Find last non-whitespace character (JSON allows trailing whitespace)
|
|
27
|
+
let lastIdx = len - 1;
|
|
28
|
+
while (lastIdx >= 0) {
|
|
29
|
+
const code = data.charCodeAt(lastIdx);
|
|
30
|
+
if (!JSON_WHITESPACE.has(code)) break;
|
|
31
|
+
lastIdx--;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Must end with matching bracket
|
|
35
|
+
const last = data.charCodeAt(lastIdx);
|
|
36
|
+
if (first === 0x7B && last !== 0x7D) return true; // {} must match - definitely NOT valid
|
|
37
|
+
if (first === 0x5B && last !== 0x5D) return true; // [] must match - definitely NOT valid
|
|
38
|
+
|
|
39
|
+
// Looks like JSON object/array, might be valid
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check if first char is a valid JSON starting character:
|
|
44
|
+
// '"' (0x22) for strings, '-' (0x2D) or digit (0x30-0x39) for numbers,
|
|
45
|
+
// 't' (0x74) for true, 'f' (0x66) for false, 'n' (0x6E) for null
|
|
46
|
+
const isValidStart = (
|
|
47
|
+
first === 0x22 || // '"'
|
|
48
|
+
first === 0x2D || // '-'
|
|
49
|
+
(first >= 0x30 && first <= 0x39) || // '0'-'9'
|
|
50
|
+
first === 0x74 || // 't' (true)
|
|
51
|
+
first === 0x66 || // 'f' (false)
|
|
52
|
+
first === 0x6E // 'n' (null)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// If it starts with a valid JSON character, it might be valid JSON
|
|
56
|
+
// Return false to indicate "might be JSON, need to parse"
|
|
57
|
+
if (isValidStart) return false;
|
|
58
|
+
|
|
59
|
+
// Doesn't start with valid JSON character - definitely NOT valid JSON
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isValidJSON(data) {
|
|
64
|
+
if (isValidJSONCheap(data)) return false;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
JSON.parse(data);
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
|
|
75
|
+
//#region JPtr Tests
|
|
76
|
+
// Single-pass char-code scanners (no regex, no allocation), shared by the
|
|
77
|
+
// `json-pointer`/`relative-json-pointer` string formats.
|
|
78
|
+
|
|
79
|
+
// scan the pointer body `str[pos..)`: '/'-delimited segments where every
|
|
80
|
+
// '~' must be followed by '0' or '1' (RFC 6901 section 3)
|
|
81
|
+
function isValidPointerBody(str, pos) {
|
|
82
|
+
const len = str.length;
|
|
83
|
+
for (; pos < len; pos++) {
|
|
84
|
+
if (str.charCodeAt(pos) === CC_TILDE) {
|
|
85
|
+
const d = pos + 1 < len ? str.charCodeAt(pos + 1) : -1;
|
|
86
|
+
if (d !== CC_0 && d !== CC_1)
|
|
87
|
+
return false;
|
|
88
|
+
pos++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// JSON-pointer: https://tools.ietf.org/html/rfc6901
|
|
95
|
+
export function isValidJSONPointer(str) {
|
|
96
|
+
if (typeof str !== 'string')
|
|
97
|
+
return false;
|
|
98
|
+
if (str.length === 0)
|
|
99
|
+
return true;
|
|
100
|
+
if (str.charCodeAt(0) !== CC_SLASH)
|
|
101
|
+
return false;
|
|
102
|
+
return isValidPointerBody(str, 1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// unreserved / sub-delims / ':' / '@' per RFC 3986 pchar, minus the
|
|
106
|
+
// pointer-significant '~' and '/' handled by the caller
|
|
107
|
+
function isFragmentPointerCode(c) {
|
|
108
|
+
return (c >= 0x61 && c <= 0x7A) // a-z
|
|
109
|
+
|| (c >= 0x41 && c <= 0x5A) // A-Z
|
|
110
|
+
|| isDigitCode(c)
|
|
111
|
+
|| (c >= 0x26 && c <= 0x2E) // & ' ( ) * + , - .
|
|
112
|
+
|| c === 0x21 // !
|
|
113
|
+
|| c === 0x24 // $
|
|
114
|
+
|| c === 0x3A // :
|
|
115
|
+
|| c === 0x3B // ;
|
|
116
|
+
|| c === 0x3D // =
|
|
117
|
+
|| c === 0x40 // @
|
|
118
|
+
|| c === 0x5F; // _
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// uri fragment: https://tools.ietf.org/html/rfc3986#appendix-A
|
|
122
|
+
export function isValidJSONPointerUriFragment(str) {
|
|
123
|
+
if (typeof str !== 'string')
|
|
124
|
+
return false;
|
|
125
|
+
const len = str.length;
|
|
126
|
+
if (len === 0 || str.charCodeAt(0) !== CC_HASH)
|
|
127
|
+
return false;
|
|
128
|
+
if (len > 1 && str.charCodeAt(1) !== CC_SLASH)
|
|
129
|
+
return false;
|
|
130
|
+
for (let pos = 1; pos < len; pos++) {
|
|
131
|
+
const c = str.charCodeAt(pos);
|
|
132
|
+
if (c === CC_SLASH || isFragmentPointerCode(c))
|
|
133
|
+
continue;
|
|
134
|
+
if (c === CC_PERCENT) { // percent-encoded octet
|
|
135
|
+
if (pos + 2 >= len
|
|
136
|
+
|| !isHexDigitCode(str.charCodeAt(pos + 1))
|
|
137
|
+
|| !isHexDigitCode(str.charCodeAt(pos + 2)))
|
|
138
|
+
return false;
|
|
139
|
+
pos += 2;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (c === CC_TILDE) {
|
|
143
|
+
const d = pos + 1 < len ? str.charCodeAt(pos + 1) : -1;
|
|
144
|
+
if (d !== CC_0 && d !== CC_1)
|
|
145
|
+
return false;
|
|
146
|
+
pos++;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// relative JSON-pointer: http://tools.ietf.org/html/draft-luff-relative-json-pointer-00
|
|
155
|
+
export function isValidRelativeJSONPointer(str) {
|
|
156
|
+
if (typeof str !== 'string')
|
|
157
|
+
return false;
|
|
158
|
+
const len = str.length;
|
|
159
|
+
if (len === 0)
|
|
160
|
+
return false;
|
|
161
|
+
const first = str.charCodeAt(0);
|
|
162
|
+
if (!isDigitCode(first))
|
|
163
|
+
return false;
|
|
164
|
+
let pos = 1;
|
|
165
|
+
if (first !== CC_0) {
|
|
166
|
+
while (pos < len && isDigitCode(str.charCodeAt(pos)))
|
|
167
|
+
pos++;
|
|
168
|
+
}
|
|
169
|
+
if (pos === len)
|
|
170
|
+
return true;
|
|
171
|
+
const c = str.charCodeAt(pos);
|
|
172
|
+
if (c === CC_HASH)
|
|
173
|
+
return pos + 1 === len;
|
|
174
|
+
if (c !== CC_SLASH)
|
|
175
|
+
return false;
|
|
176
|
+
return isValidPointerBody(str, pos + 1);
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
|
|
180
|
+
//#region JSONPath Tests (RFC 9535)
|
|
181
|
+
// JSONPath syntax: https://www.rfc-editor.org/rfc/rfc9535.html
|
|
182
|
+
|
|
183
|
+
// Basic JSONPath structure:
|
|
184
|
+
// $ - root
|
|
185
|
+
// $.store.book[0].title - dot notation with array index
|
|
186
|
+
// $['store']['book'][0] - bracket notation
|
|
187
|
+
// $..name - recursive descent
|
|
188
|
+
// $.* - wildcard
|
|
189
|
+
// $[?(@.price < 10)] - filter expression
|
|
190
|
+
// $[0:5:2] - slice
|
|
191
|
+
// $[0,1,2] - multiple indices
|
|
192
|
+
|
|
193
|
+
// Simplified regex that covers common JSONPath patterns
|
|
194
|
+
// This validates the structure without fully parsing filter expressions
|
|
195
|
+
// Pattern breakdown:
|
|
196
|
+
// (\$|@) - starts with $ or @
|
|
197
|
+
// (?:\.\.[a-zA-Z_][a-zA-Z0-9_]*| - recursive descent with name (e.g., $..name)
|
|
198
|
+
// \.\.| - recursive descent (e.g., $..)
|
|
199
|
+
// \.[a-zA-Z_][a-zA-Z0-9_]*| - dot notation (e.g., $.store)
|
|
200
|
+
// \.[*]| - dot wildcard (e.g., $.*)
|
|
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*'(?:[^'\\]|\\.)*')*|\"(?:[^"\\]|\\.)*\"(?:\s*,\s*\"(?:[^"\\]|\\.)*\")*)\s*\])*$/;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Validates a JSONPath expression string per RFC 9535.
|
|
206
|
+
*
|
|
207
|
+
* JSONPath syntax includes:
|
|
208
|
+
* - $ - root node selector
|
|
209
|
+
* - @ - current node selector (used in filter expressions)
|
|
210
|
+
* - .name - dot notation for child member
|
|
211
|
+
* - ['name'] or ["name"] - bracket notation for child member
|
|
212
|
+
* - [index] - array index
|
|
213
|
+
* - [*] - wildcard selector
|
|
214
|
+
* - [start:end:step] - array slice
|
|
215
|
+
* - [?expression] - filter expression
|
|
216
|
+
* - .. - recursive descent
|
|
217
|
+
*
|
|
218
|
+
* @param {string} str - The JSONPath expression to validate
|
|
219
|
+
* @returns {boolean} - True if the string is a valid JSONPath expression
|
|
220
|
+
* @example
|
|
221
|
+
* isValidJSONPath('$.store.book[0].title'); // true
|
|
222
|
+
* isValidJSONPath('$..name'); // true
|
|
223
|
+
* isValidJSONPath('$[*]'); // true
|
|
224
|
+
* isValidJSONPath('$[?(@.price < 10)]'); // true
|
|
225
|
+
* isValidJSONPath('$.store.book[0:5]'); // true
|
|
226
|
+
* isValidJSONPath('@.name'); // true (current node selector)
|
|
227
|
+
* isValidJSONPath('store'); // false (must start with $ or @)
|
|
228
|
+
* isValidJSONPath(''); // false (empty string)
|
|
229
|
+
*/
|
|
230
|
+
export function isValidJSONPath(str) {
|
|
231
|
+
if (typeof str !== 'string')
|
|
232
|
+
return false;
|
|
233
|
+
|
|
234
|
+
// Must start with $ (root) or @ (current node in filters)
|
|
235
|
+
if (!str || (str[0] !== '$' && str[0] !== '@'))
|
|
236
|
+
return false;
|
|
237
|
+
|
|
238
|
+
// Single $ or @ is valid
|
|
239
|
+
if (str.length === 1)
|
|
240
|
+
return true;
|
|
241
|
+
|
|
242
|
+
// Use the regex for basic validation
|
|
243
|
+
if (!CONST_REGEXP_JSONPATH.test(str))
|
|
244
|
+
return false;
|
|
245
|
+
|
|
246
|
+
// Additional validation for bracket contents
|
|
247
|
+
return validateJSONPathBrackets(str);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Validates that brackets in JSONPath are balanced and well-formed
|
|
252
|
+
* @param {string} str - The JSONPath string to validate
|
|
253
|
+
* @returns {boolean} - True if brackets are valid
|
|
254
|
+
* @private
|
|
255
|
+
*/
|
|
256
|
+
function validateJSONPathBrackets(str) {
|
|
257
|
+
let depth = 0;
|
|
258
|
+
let inString = false;
|
|
259
|
+
let stringChar = null;
|
|
260
|
+
let escapeNext = false;
|
|
261
|
+
|
|
262
|
+
for (let i = 0; i < str.length; i++) {
|
|
263
|
+
const char = str[i];
|
|
264
|
+
|
|
265
|
+
if (escapeNext) {
|
|
266
|
+
escapeNext = false;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (char === '\\') {
|
|
271
|
+
escapeNext = true;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (inString) {
|
|
276
|
+
if (char === stringChar) {
|
|
277
|
+
inString = false;
|
|
278
|
+
stringChar = null;
|
|
279
|
+
}
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (char === "'" || char === '"') {
|
|
284
|
+
inString = char;
|
|
285
|
+
stringChar = char;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (char === '[') {
|
|
290
|
+
depth++;
|
|
291
|
+
} else if (char === ']') {
|
|
292
|
+
depth--;
|
|
293
|
+
if (depth < 0)
|
|
294
|
+
return false; // Unbalanced brackets
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return depth === 0 && !inString;
|
|
299
|
+
}
|
|
300
|
+
//#endregion
|
package/src/index.js
ADDED