json-web-streams 0.0.1
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/LICENSE +209 -0
- package/README.md +367 -0
- package/dist/JSONParserStream.d.ts +18 -0
- package/dist/JSONParserStream.js +108 -0
- package/dist/JSONParserText.d.ts +47 -0
- package/dist/JSONParserText.js +261 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/jsonPathToQueryPath.d.ts +5 -0
- package/dist/jsonPathToQueryPath.js +33 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction, and
|
|
10
|
+
distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by the
|
|
13
|
+
copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all other
|
|
16
|
+
entities that control, are controlled by, or are under common control with
|
|
17
|
+
that entity. For the purposes of this definition, "control" means (i) the
|
|
18
|
+
power, direct or indirect, to cause the direction or management of such
|
|
19
|
+
entity, whether by contract or otherwise, or (ii) ownership of
|
|
20
|
+
fifty percent (50%) or more of the outstanding shares, or (iii) beneficial
|
|
21
|
+
ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity exercising
|
|
24
|
+
permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation source,
|
|
28
|
+
and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical transformation
|
|
31
|
+
or translation of a Source form, including but not limited to compiled
|
|
32
|
+
object code, generated documentation, and conversions to
|
|
33
|
+
other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or Object
|
|
36
|
+
form, made available under the License, as indicated by a copyright notice
|
|
37
|
+
that is included in or attached to the work (an example is provided in the
|
|
38
|
+
Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object form,
|
|
41
|
+
that is based on (or derived from) the Work and for which the editorial
|
|
42
|
+
revisions, annotations, elaborations, or other modifications represent,
|
|
43
|
+
as a whole, an original work of authorship. For the purposes of this
|
|
44
|
+
License, Derivative Works shall not include works that remain separable
|
|
45
|
+
from, or merely link (or bind by name) to the interfaces of, the Work and
|
|
46
|
+
Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including the original
|
|
49
|
+
version of the Work and any modifications or additions to that Work or
|
|
50
|
+
Derivative Works thereof, that is intentionally submitted to Licensor for
|
|
51
|
+
inclusion in the Work by the copyright owner or by an individual or
|
|
52
|
+
Legal Entity authorized to submit on behalf of the copyright owner.
|
|
53
|
+
For the purposes of this definition, "submitted" means any form of
|
|
54
|
+
electronic, verbal, or written communication sent to the Licensor or its
|
|
55
|
+
representatives, including but not limited to communication on electronic
|
|
56
|
+
mailing lists, source code control systems, and issue tracking systems
|
|
57
|
+
that are managed by, or on behalf of, the Licensor for the purpose of
|
|
58
|
+
discussing and improving the Work, but excluding communication that is
|
|
59
|
+
conspicuously marked or otherwise designated in writing by the copyright
|
|
60
|
+
owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity on
|
|
63
|
+
behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License.
|
|
67
|
+
|
|
68
|
+
Subject to the terms and conditions of this License, each Contributor
|
|
69
|
+
hereby grants to You a perpetual, worldwide, non-exclusive, no-charge,
|
|
70
|
+
royalty-free, irrevocable copyright license to reproduce, prepare
|
|
71
|
+
Derivative Works of, publicly display, publicly perform, sublicense,
|
|
72
|
+
and distribute the Work and such Derivative Works in
|
|
73
|
+
Source or Object form.
|
|
74
|
+
|
|
75
|
+
3. Grant of Patent License.
|
|
76
|
+
|
|
77
|
+
Subject to the terms and conditions of this License, each Contributor
|
|
78
|
+
hereby grants to You a perpetual, worldwide, non-exclusive, no-charge,
|
|
79
|
+
royalty-free, irrevocable (except as stated in this section) patent
|
|
80
|
+
license to make, have made, use, offer to sell, sell, import, and
|
|
81
|
+
otherwise transfer the Work, where such license applies only to those
|
|
82
|
+
patent claims licensable by such Contributor that are necessarily
|
|
83
|
+
infringed by their Contribution(s) alone or by combination of their
|
|
84
|
+
Contribution(s) with the Work to which such Contribution(s) was submitted.
|
|
85
|
+
If You institute patent litigation against any entity (including a
|
|
86
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
|
87
|
+
Contribution incorporated within the Work constitutes direct or
|
|
88
|
+
contributory patent infringement, then any patent licenses granted to
|
|
89
|
+
You under this License for that Work shall terminate as of the date such
|
|
90
|
+
litigation is filed.
|
|
91
|
+
|
|
92
|
+
4. Redistribution.
|
|
93
|
+
|
|
94
|
+
You may reproduce and distribute copies of the Work or Derivative Works
|
|
95
|
+
thereof in any medium, with or without modifications, and in Source or
|
|
96
|
+
Object form, provided that You meet the following conditions:
|
|
97
|
+
|
|
98
|
+
1. You must give any other recipients of the Work or Derivative Works a
|
|
99
|
+
copy of this License; and
|
|
100
|
+
|
|
101
|
+
2. You must cause any modified files to carry prominent notices stating
|
|
102
|
+
that You changed the files; and
|
|
103
|
+
|
|
104
|
+
3. You must retain, in the Source form of any Derivative Works that You
|
|
105
|
+
distribute, all copyright, patent, trademark, and attribution notices from
|
|
106
|
+
the Source form of the Work, excluding those notices that do not pertain
|
|
107
|
+
to any part of the Derivative Works; and
|
|
108
|
+
|
|
109
|
+
4. If the Work includes a "NOTICE" text file as part of its distribution,
|
|
110
|
+
then any Derivative Works that You distribute must include a readable copy
|
|
111
|
+
of the attribution notices contained within such NOTICE file, excluding
|
|
112
|
+
those notices that do not pertain to any part of the Derivative Works,
|
|
113
|
+
in at least one of the following places: within a NOTICE text file
|
|
114
|
+
distributed as part of the Derivative Works; within the Source form or
|
|
115
|
+
documentation, if provided along with the Derivative Works; or, within a
|
|
116
|
+
display generated by the Derivative Works, if and wherever such
|
|
117
|
+
third-party notices normally appear. The contents of the NOTICE file are
|
|
118
|
+
for informational purposes only and do not modify the License.
|
|
119
|
+
You may add Your own attribution notices within Derivative Works that You
|
|
120
|
+
distribute, alongside or as an addendum to the NOTICE text from the Work,
|
|
121
|
+
provided that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and may
|
|
125
|
+
provide additional or different license terms and conditions for use,
|
|
126
|
+
reproduction, or distribution of Your modifications, or for any such
|
|
127
|
+
Derivative Works as a whole, provided Your use, reproduction, and
|
|
128
|
+
distribution of the Work otherwise complies with the conditions
|
|
129
|
+
stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions.
|
|
132
|
+
|
|
133
|
+
Unless You explicitly state otherwise, any Contribution intentionally
|
|
134
|
+
submitted for inclusion in the Work by You to the Licensor shall be under
|
|
135
|
+
the terms and conditions of this License, without any additional
|
|
136
|
+
terms or conditions. Notwithstanding the above, nothing herein shall
|
|
137
|
+
supersede or modify the terms of any separate license agreement you may
|
|
138
|
+
have executed with Licensor regarding such Contributions.
|
|
139
|
+
|
|
140
|
+
6. Trademarks.
|
|
141
|
+
|
|
142
|
+
This License does not grant permission to use the trade names, trademarks,
|
|
143
|
+
service marks, or product names of the Licensor, except as required for
|
|
144
|
+
reasonable and customary use in describing the origin of the Work and
|
|
145
|
+
reproducing the content of the NOTICE file.
|
|
146
|
+
|
|
147
|
+
7. Disclaimer of Warranty.
|
|
148
|
+
|
|
149
|
+
Unless required by applicable law or agreed to in writing, Licensor
|
|
150
|
+
provides the Work (and each Contributor provides its Contributions)
|
|
151
|
+
on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
|
152
|
+
either express or implied, including, without limitation, any warranties
|
|
153
|
+
or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS
|
|
154
|
+
FOR A PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
155
|
+
appropriateness of using or redistributing the Work and assume any risks
|
|
156
|
+
associated with Your exercise of permissions under this License.
|
|
157
|
+
|
|
158
|
+
8. Limitation of Liability.
|
|
159
|
+
|
|
160
|
+
In no event and under no legal theory, whether in tort
|
|
161
|
+
(including negligence), contract, or otherwise, unless required by
|
|
162
|
+
applicable law (such as deliberate and grossly negligent acts) or agreed
|
|
163
|
+
to in writing, shall any Contributor be liable to You for damages,
|
|
164
|
+
including any direct, indirect, special, incidental, or consequential
|
|
165
|
+
damages of any character arising as a result of this License or out of
|
|
166
|
+
the use or inability to use the Work (including but not limited to damages
|
|
167
|
+
for loss of goodwill, work stoppage, computer failure or malfunction,
|
|
168
|
+
or any and all other commercial damages or losses), even if such
|
|
169
|
+
Contributor has been advised of the possibility of such damages.
|
|
170
|
+
|
|
171
|
+
9. Accepting Warranty or Additional Liability.
|
|
172
|
+
|
|
173
|
+
While redistributing the Work or Derivative Works thereof, You may choose
|
|
174
|
+
to offer, and charge a fee for, acceptance of support, warranty,
|
|
175
|
+
indemnity, or other liability obligations and/or rights consistent with
|
|
176
|
+
this License. However, in accepting such obligations, You may act only
|
|
177
|
+
on Your own behalf and on Your sole responsibility, not on behalf of any
|
|
178
|
+
other Contributor, and only if You agree to indemnify, defend, and hold
|
|
179
|
+
each Contributor harmless for any liability incurred by, or claims
|
|
180
|
+
asserted against, such Contributor by reason of your accepting any such
|
|
181
|
+
warranty or additional liability.
|
|
182
|
+
|
|
183
|
+
END OF TERMS AND CONDITIONS
|
|
184
|
+
|
|
185
|
+
APPENDIX: How to apply the Apache License to your work
|
|
186
|
+
|
|
187
|
+
To apply the Apache License to your work, attach the following boilerplate
|
|
188
|
+
notice, with the fields enclosed by brackets "[]" replaced with your own
|
|
189
|
+
identifying information. (Don't include the brackets!) The text should be
|
|
190
|
+
enclosed in the appropriate comment syntax for the file format. We also
|
|
191
|
+
recommend that a file or class name and description of purpose be included
|
|
192
|
+
on the same "printed page" as the copyright notice for easier
|
|
193
|
+
identification within third-party archives.
|
|
194
|
+
|
|
195
|
+
Copyright 2017 Jeremy Scheff
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
199
|
+
you may not use this file except in compliance with the License.
|
|
200
|
+
You may obtain a copy of the License at
|
|
201
|
+
|
|
202
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
203
|
+
|
|
204
|
+
Unless required by applicable law or agreed to in writing, software
|
|
205
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
206
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
207
|
+
or implied. See the License for the specific language governing
|
|
208
|
+
permissions and limitations under the License.
|
|
209
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# json-web-streams
|
|
2
|
+
|
|
3
|
+
- **Stream large JSON files** without loading everything into memory
|
|
4
|
+
- Built on the **Web Streams API** so it runs in web browsers, Node.js, and more
|
|
5
|
+
- Query with **JSONPath** to extract only the data you need
|
|
6
|
+
- Integrated **schema validation** with full **TypeScript** support
|
|
7
|
+
- Tested on the [JSON Parsing Test Suite](https://github.com/nst/JSONTestSuite) and other edge cases
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install json-web-streams
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Getting started
|
|
16
|
+
|
|
17
|
+
Imagine you have some JSON like:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
[{ "x": 1 }, { "x": 2 }, { "x": 3 }]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
You can just `JSON.parse` it and then do whatever you want. But what if it's so large that parsing it all at once is slow or impossible?
|
|
24
|
+
|
|
25
|
+
By using json-web-streams, you can stream through the JSON object without having to read it all into memory. Here's an example that prints out each object as it is parsed:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { JSONParseStream } from "json-web-streams";
|
|
29
|
+
|
|
30
|
+
// data.json contains: [{ "x": 1 }, { "x": 2 }, { "x": 3 }]
|
|
31
|
+
const response = await fetch("https://example.com/data.json");
|
|
32
|
+
await response.body
|
|
33
|
+
.pipeThrough(new TextDecoderStream())
|
|
34
|
+
.pipeThrough(new JSONParseStream(["$[*]"]))
|
|
35
|
+
.pipeTo(
|
|
36
|
+
new WritableStream({
|
|
37
|
+
write({ value }) {
|
|
38
|
+
console.log(value);
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Output:
|
|
44
|
+
// {"x": 1}
|
|
45
|
+
// {"x": 2}
|
|
46
|
+
// {"x": 3}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> [!TIP]
|
|
50
|
+
> If you don't have to support Safari, [most other environments](https://caniuse.com/mdn-api_readablestream_--asynciterator) let you use a nicer syntax for consuming stream output as an async iterator:
|
|
51
|
+
>
|
|
52
|
+
> ```ts
|
|
53
|
+
> const stream = response.body
|
|
54
|
+
> .pipeThrough(new TextDecoderStream())
|
|
55
|
+
> .pipeThrough(new JSONParseStream(["$[*]"]));
|
|
56
|
+
> for await (const { value } of stream) {
|
|
57
|
+
> console.log(value);
|
|
58
|
+
> }
|
|
59
|
+
> ```
|
|
60
|
+
|
|
61
|
+
## API
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const jsonParseStream = new JSONParseStream(
|
|
65
|
+
jsonPaths: (JSONPath | { path: JSONPath; schema: StandardSchemaV1 })[],
|
|
66
|
+
options?: { multi?: boolean },
|
|
67
|
+
);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### `jsonPaths: (JSONPath | { path: JSONPath; schema: StandardSchemaV1 })[]`
|
|
71
|
+
|
|
72
|
+
The first argument to `JSONParseStream` is an array specifying what objects to emit from the stream. The `JSONPath` type is a string containing a JSONPath query. JSONPath is a query language for JSON to let you pick out specific values from a JSON object.
|
|
73
|
+
|
|
74
|
+
(The `JSONPath` type is just a slightly more restrictive version of a string. I wish it could completely parse and validate the JSONPath syntax, but currently it just enforces some little things like that it must start with a `$`.)
|
|
75
|
+
|
|
76
|
+
In many cases, you'll just have one JSONPath query, like:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
["$.foo[*]"]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
but you can have as many as you want:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
["$.foo[*]", "$.bar", "$.bar.baz"]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
> [!IMPORTANT]
|
|
89
|
+
> **json-web-streams only supports a subset of JSONPath.** See the [JSONPath](#jsonpath) section below for more details and examples. But to briefly explain what a typical JSONPath query means:
|
|
90
|
+
>
|
|
91
|
+
> `$.foo[*]` can be broken into three parts:
|
|
92
|
+
>
|
|
93
|
+
> `$` refers to the root node of the JSON object\
|
|
94
|
+
> `.foo` is similar to accessing an object property in JS, so this is the `foo` property of the root node\
|
|
95
|
+
> `[*]` means "every value in this array or object"
|
|
96
|
+
>
|
|
97
|
+
> In total this query means "emit every value in the array/object in the `foo` property of the overall JSON object". So for this JSON `{ foo: ["A", "B", "C"] }` it would emit the three values `"A"`, `"B"`, and `"C"`.
|
|
98
|
+
|
|
99
|
+
The values of the `jsonPaths` array can either be `JSONPath` strings, or objects like `{ path: JSONPath; schema: StandardSchemaV1 }` where `schema` is a schema validator from any library supporting the [Standard Schema specification](https://github.com/standard-schema/standard-schema) such as Zod, Valibot, or ArkType. When you supply a schema like this, each value will be validated before it is emitted by the stream, and emitted values will have correct TypeScript types rather than being `unknown`. For more details, see the [Schema validation and types for `JSONParseStream` output](#schema-validation-and-types-for-jsonparsestream-output) section below.
|
|
100
|
+
|
|
101
|
+
### `options?: { multi?: boolean }`
|
|
102
|
+
|
|
103
|
+
There are various [JSON streaming formats](https://en.wikipedia.org/wiki/JSON_streaming) that make it more convenient to stream JSON by omitting the opening/closing tags and instead emitting multiple JSON objects sequentially. Some of these formats are:
|
|
104
|
+
|
|
105
|
+
- JSON Lines (JSONL) aka Newline Delimited JSON (NDJSON) - JSON objects are separated by \n
|
|
106
|
+
- JSON Text Sequences aka json-seq - JSON objects are separated by the unicode record separator character ␞
|
|
107
|
+
- Concatenated JSON - JSON objects are simply concatenated with nothing in between.
|
|
108
|
+
|
|
109
|
+
Setting `multi` to `true` enables support for all of those streaming JSON formats. It's actually a little more permissive - it allows any combination of whitespace and the unicode record separator between JSON objects.
|
|
110
|
+
|
|
111
|
+
> [!TIP]
|
|
112
|
+
> If you want to emit every one of these individual JSON objects, use the JSONPath query `$` which means "emit the entire object", so in `multi` mode it will emit each of the individual objects.
|
|
113
|
+
|
|
114
|
+
### `JSONParseStream` input
|
|
115
|
+
|
|
116
|
+
`new JSONParseStream(jsonPaths)` returns a [TransformStream](https://developer.mozilla.org/en-US/docs/Web/API/TransformStream), meaning that it receives some input (e.g. from a ReadableStream) and emits some output (e.g. to a WritableStream).
|
|
117
|
+
|
|
118
|
+
Inputs to the `JSONParseStream` stream must be strings. If you have a stream emitting some binary encoded text (such as from `fetch`), pipe it through `TextDecoderStream` first:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const response = await fetch("https://example.com/data.json");
|
|
122
|
+
const stream = response.body
|
|
123
|
+
.pipeThrough(new TextDecoderStream())
|
|
124
|
+
.pipeThrough(new JSONParseStream(["$.foo[*]"]));
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `JSONParseStream` output
|
|
128
|
+
|
|
129
|
+
Output from `JSONParseStream` has this format:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
type JSONParseStreamOutput<T = unknown> = {
|
|
133
|
+
value: T;
|
|
134
|
+
path: JSONPath;
|
|
135
|
+
wildcardKeys?: string[];
|
|
136
|
+
};
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`value` is the value selected by one of your JSONPath queries.
|
|
140
|
+
|
|
141
|
+
`path` is the JSONPath query (from the `jsonPaths` parameter of `JSONParseStream`) that matched `value`.
|
|
142
|
+
|
|
143
|
+
If you only have one JSONPath query, you can ignore `path`. But if you have more than one, `path` may be helpful when processing stream output to distinguish between different types of values. For example:
|
|
144
|
+
|
|
145
|
+
<!-- prettier-ignore -->
|
|
146
|
+
```ts
|
|
147
|
+
await new ReadableStream({
|
|
148
|
+
start(controller) {
|
|
149
|
+
controller.enqueue('{ "foo": [1, 2], "bar": ["a", "b", "c"] }');
|
|
150
|
+
controller.close();
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
.pipeThrough(new JSONParseStream(["$.bar[*]", "$.foo[*]"]))
|
|
154
|
+
.pipeTo(
|
|
155
|
+
new WritableStream({
|
|
156
|
+
write(record) {
|
|
157
|
+
if (record.path === "$.bar[*]") {
|
|
158
|
+
// Do something with the values from bar
|
|
159
|
+
} else {
|
|
160
|
+
// Do something with the values from foo
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`wildcardKeys` is defined when you have a wildcard in an object (not an array) somewhere in your JSONPath. For example:
|
|
168
|
+
|
|
169
|
+
<!-- prettier-ignore -->
|
|
170
|
+
```ts
|
|
171
|
+
await new ReadableStream({
|
|
172
|
+
start(controller) {
|
|
173
|
+
controller.enqueue('{ "foo": [1, 2], "bar": ["a", "b", "c"] }');
|
|
174
|
+
controller.close();
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
.pipeThrough(new JSONParseStream(["$[*]"]))
|
|
178
|
+
.pipeTo(
|
|
179
|
+
new WritableStream({
|
|
180
|
+
write(record) {
|
|
181
|
+
console.log(record);
|
|
182
|
+
},
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
// Output:
|
|
186
|
+
// { path: "$[*]", value: [1, 2], wildcardKeys: ["foo"] },
|
|
187
|
+
// { path: "$[*]", value: ["a", "b", "c"], wildcardKeys: ["bar"] },
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The purpose of `wildcardKeys` is to allow you to easily distinguish different types of objects. `wildcardKeys` has one entry for each wildcard object in your JSONPath query.
|
|
191
|
+
|
|
192
|
+
> [!WARNING]
|
|
193
|
+
> It is possible to have two JSONPath queries that output overlapping objects, like if your data is `{ "foo": [1, 2] }` and you query for both `$` and `$.foo`. This will emit two objects: `{ foo: [1, 2] }` and `[1, 2]`. Due to how json-web-streams works internally, both of those objects share the same array instance, meaning that if the array in one is mutated it will affect the other.
|
|
194
|
+
>
|
|
195
|
+
> Some schema validation libraries do a deep clone of objects they validate. In that case, you won't have this issue. Otherwise, in the rare case that you query for overlapping objects, you will have to handle this problem, such as by deep cloning one of the objects.
|
|
196
|
+
|
|
197
|
+
#### Schema validation and types for `JSONParseStream` output
|
|
198
|
+
|
|
199
|
+
If you want to validate the objects as they stream in, json-web-streams integrates with any schema validation library that supports the [Standard Schema specification](https://github.com/standard-schema/standard-schema), such as Zod, Valibot, and ArkType.
|
|
200
|
+
|
|
201
|
+
To use schema validation for a JSONPath query, then pass an object `{ path: JSONPath; schema: StandardSchemaV1 }` rather just a string `JSONPath`. Then each value will be validated before being output by the stream, and the correct TypeScript types will be propagated through the stream as well.
|
|
202
|
+
|
|
203
|
+
<!-- prettier-ignore -->
|
|
204
|
+
```ts
|
|
205
|
+
import * as z from "zod";
|
|
206
|
+
|
|
207
|
+
await new ReadableStream({
|
|
208
|
+
start(controller) {
|
|
209
|
+
controller.enqueue('{ "foo": [1, 2], "bar": ["a", "b", "c"] }');
|
|
210
|
+
controller.close();
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
.pipeThrough(
|
|
214
|
+
new JSONParseStream([
|
|
215
|
+
{ path: "$.foo[*]", schema: z.number() },
|
|
216
|
+
{ path: "$.bar[*]", schema: z.string() },
|
|
217
|
+
]),
|
|
218
|
+
)
|
|
219
|
+
.pipeTo(
|
|
220
|
+
new WritableStream({
|
|
221
|
+
write(record) {
|
|
222
|
+
if (record.path === "$.foo[*]") {
|
|
223
|
+
// Type of record.value is number
|
|
224
|
+
} else {
|
|
225
|
+
// Type of record.value is string
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
> [!TIP]
|
|
233
|
+
> If you only want to validate some values, you can mix `{ path: JSONPath; schema: StandardSchemaV1 }` and `JSONPath` in the `jsonPaths` array.
|
|
234
|
+
|
|
235
|
+
For JSONPath queries with no schema, emitted values will have the `unknown` type.
|
|
236
|
+
|
|
237
|
+
## JSONPath
|
|
238
|
+
|
|
239
|
+
json-web-streams supports a subset of JSONPath. Currently the supported components are:
|
|
240
|
+
|
|
241
|
+
- **The root node**, represented by the symbol `$` which must be the first character of any JSONPath query.
|
|
242
|
+
|
|
243
|
+
- **Name selectors** which are like accessing a property in a JS object. For instance if you have an object like `{ "foo": { bar: 5 } }`, then `$.foo.bar` refers to the value `5`. You can also write this in the more verbose bracket notation like `$["foo"]["bar"]` or `$["foo", "bar"]`, which is useful if your key names include characters that need escaping. You can also mix them like `$.foo["bar"]` or use single quotes like `$['foo']['bar']` - all of these JSONPath queries have the same meaning.
|
|
244
|
+
|
|
245
|
+
- **Wildcard selectors** which select every value in an array or object. With this JSON `{ "foo": { "a": 1, "b": 2, "c": 3 } }`, the JSONPath query `$.foo[*]` would emit the three individual numbers `1`, `2`, and `3`. If the inner object was changed to an array like `{ "foo": [1, 2, 3] }`, the same JSONPath query would emit the same values.
|
|
246
|
+
|
|
247
|
+
You can combine these selectors as deep as you want. For instance, if instead you have an array of objects you can select values inside those individual objects with a query like `$.foo[*].bar`. Applying that to this data:
|
|
248
|
+
|
|
249
|
+
```json
|
|
250
|
+
{ "foo": [{ "bar": 1 }, { "bar": 2 }, { "bar": 3 }] }
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
would emit `1`, `2`, and `3`.
|
|
254
|
+
|
|
255
|
+
Or if the array is at the root if the object like this data:
|
|
256
|
+
|
|
257
|
+
```json
|
|
258
|
+
[{ "x": 1 }, { "x": 2 }, { "x": 3 }]
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
then you'd write something like `$[*]` to emit each object (`{ x: 1}`, `{x: 2}`, `{x: 3}`), or `$[*].key` to emit just the numbers (`1`, `2`, `3`).
|
|
262
|
+
|
|
263
|
+
To emit the whole object at once (okay in that case you wouldn't use this library, but maybe just for testing, or for `multi` mode) you just use `$`.
|
|
264
|
+
|
|
265
|
+
> [!TIP]
|
|
266
|
+
> If you want to play around with JSONPath queries to make sure you understand what they are doing, [jsonpath.com](https://jsonpath.com/) is a great website that lets you easily run a JSONPath query on some data.
|
|
267
|
+
|
|
268
|
+
## JSONParseStream examples
|
|
269
|
+
|
|
270
|
+
There are several examples above in code blocks throughout the README, and here are a few more!
|
|
271
|
+
|
|
272
|
+
### `wildcardKeys` vs. multiple entries in `jsonPaths`
|
|
273
|
+
|
|
274
|
+
Sometimes there are multiple ways to achieve your goal.
|
|
275
|
+
|
|
276
|
+
Let's say you have this JSON:
|
|
277
|
+
|
|
278
|
+
```json
|
|
279
|
+
{ "foo": [1, 2], "bar": ["a", "b", "c"] }
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
You want to get all the values in `foo` and all the values in `bar`. You could define them as two separate JSONPath queries and then distinguish the output with `.path`:
|
|
283
|
+
|
|
284
|
+
<!-- prettier-ignore -->
|
|
285
|
+
```ts
|
|
286
|
+
await new ReadableStream({
|
|
287
|
+
start(controller) {
|
|
288
|
+
controller.enqueue('{ "foo": [1, 2], "bar": ["a", "b", "c"] }');
|
|
289
|
+
controller.close();
|
|
290
|
+
},
|
|
291
|
+
})
|
|
292
|
+
.pipeThrough(
|
|
293
|
+
new JSONParseStream(["$.foo[*]", "$.bar[*]"]),
|
|
294
|
+
)
|
|
295
|
+
.pipeTo(
|
|
296
|
+
new WritableStream({
|
|
297
|
+
write(record) {
|
|
298
|
+
if (record.path === "$.foo[*]") {
|
|
299
|
+
// 1, 2
|
|
300
|
+
} else {
|
|
301
|
+
// a, b, c
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Or you could use one JSONPath query with a wildcard, and then use `.wildcardKeys` to distinguish the objects:
|
|
309
|
+
|
|
310
|
+
<!-- prettier-ignore -->
|
|
311
|
+
```ts
|
|
312
|
+
await new ReadableStream({
|
|
313
|
+
start(controller) {
|
|
314
|
+
controller.enqueue('{ "foo": [1, 2], "bar": ["a", "b", "c"] }');
|
|
315
|
+
controller.close();
|
|
316
|
+
},
|
|
317
|
+
})
|
|
318
|
+
.pipeThrough(
|
|
319
|
+
new JSONParseStream(["$[*][*]"]),
|
|
320
|
+
)
|
|
321
|
+
.pipeTo(
|
|
322
|
+
new WritableStream({
|
|
323
|
+
write(record) {
|
|
324
|
+
if (record.wildcardKeys[0] === "foo") {
|
|
325
|
+
// 1, 2
|
|
326
|
+
} else {
|
|
327
|
+
// a, b, c
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Using multiple JSONPath queries is a little more explicit, but using wildcard keys is more concise, especially if you had more than just two types of objects. And instead of known keys like `foo` and `bar` your JSON had some unknown keys, then using a wildcard would be your only option.
|
|
335
|
+
|
|
336
|
+
But a nice thing about multiple JSONPath queries is that you can add schema validation to ensure your data is the correct format and give you nice TypeScript types. Whereas if you are using `wildcardKeys` to distinguish types, there is currently no way to use that information in schema validation.
|
|
337
|
+
|
|
338
|
+
In this example, Zod schemas enforce that `record.value` is either a `string` or `number` as appropriate, rather than `unknown`:
|
|
339
|
+
|
|
340
|
+
<!-- prettier-ignore -->
|
|
341
|
+
```ts
|
|
342
|
+
import * as z from "zod";
|
|
343
|
+
|
|
344
|
+
await new ReadableStream({
|
|
345
|
+
start(controller) {
|
|
346
|
+
controller.enqueue('{ "foo": [1, 2], "bar": ["a", "b", "c"] }');
|
|
347
|
+
controller.close();
|
|
348
|
+
},
|
|
349
|
+
})
|
|
350
|
+
.pipeThrough(
|
|
351
|
+
new JSONParseStream([
|
|
352
|
+
{ path: "$.foo[*]", schema: z.number() },
|
|
353
|
+
{ path: "$.bar[*]", schema: z.string() },
|
|
354
|
+
]),
|
|
355
|
+
)
|
|
356
|
+
.pipeTo(
|
|
357
|
+
new WritableStream({
|
|
358
|
+
write(record) {
|
|
359
|
+
if (record.path === "$.foo[*]") {
|
|
360
|
+
// 1, 2
|
|
361
|
+
} else {
|
|
362
|
+
// a, b, c
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
}),
|
|
366
|
+
);
|
|
367
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { JSONParserText } from "./JSONParserText.js";
|
|
2
|
+
import { JSONPath } from "./jsonPathToQueryPath.js";
|
|
3
|
+
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
4
|
+
|
|
5
|
+
//#region src/JSONParserStream.d.ts
|
|
6
|
+
declare const createJSONParserStream: <JSONPathsObject extends Partial<Record<JSONPath, StandardSchemaV1 | null | undefined>> = Record<JSONPath, never>>(jsonPaths: JSONPathsObject) => JSONParserStream<JSONPathsObject>;
|
|
7
|
+
declare class JSONParserStream<JSONPathsObject extends Partial<Record<JSONPath, StandardSchemaV1 | null | undefined>>> extends TransformStream<string, {
|
|
8
|
+
jsonPath: JSONPath;
|
|
9
|
+
value: unknown;
|
|
10
|
+
wildcardKeys?: string[];
|
|
11
|
+
}> {
|
|
12
|
+
_parser: JSONParserText;
|
|
13
|
+
constructor(jsonPaths: JSONPathsObject, options?: {
|
|
14
|
+
multi?: boolean;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { createJSONParserStream };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import JSONParserText_default from "./JSONParserText.js";
|
|
2
|
+
import { jsonPathToQueryPath } from "./jsonPathToQueryPath.js";
|
|
3
|
+
|
|
4
|
+
//#region src/JSONParserStream.ts
|
|
5
|
+
const stackToQueryPath = (stack) => {
|
|
6
|
+
return stack.slice(1).map((row) => {
|
|
7
|
+
if (row.mode === "OBJECT" && row.key !== void 0) return {
|
|
8
|
+
type: "key",
|
|
9
|
+
value: row.key
|
|
10
|
+
};
|
|
11
|
+
if (row.mode === "ARRAY") return { type: "wildcard" };
|
|
12
|
+
throw new Error(`Unexpected mode "${row.mode}"`);
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
const isEqual = (x, y) => {
|
|
16
|
+
if (!y) return false;
|
|
17
|
+
if (x.type === y?.type) {
|
|
18
|
+
if (x.type === "wildcard") return true;
|
|
19
|
+
if (x.value === y.value) return true;
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return x.type === "wildcard";
|
|
23
|
+
};
|
|
24
|
+
const createJSONParserStream = (jsonPaths) => {
|
|
25
|
+
return new JSONParserStream(jsonPaths);
|
|
26
|
+
};
|
|
27
|
+
var JSONParserStream = class extends TransformStream {
|
|
28
|
+
_parser;
|
|
29
|
+
constructor(jsonPaths, options) {
|
|
30
|
+
let parser;
|
|
31
|
+
const multi = options?.multi ?? false;
|
|
32
|
+
const queryInfos = /* @__PURE__ */ new Map();
|
|
33
|
+
for (const [key, schema] of Object.entries(jsonPaths)) {
|
|
34
|
+
const jsonPath = key;
|
|
35
|
+
const queryPath = jsonPathToQueryPath(jsonPath);
|
|
36
|
+
let wildcardIndexes;
|
|
37
|
+
for (const [i, component] of queryPath.entries()) if (component.type === "wildcard") {
|
|
38
|
+
if (wildcardIndexes === void 0) wildcardIndexes = [];
|
|
39
|
+
wildcardIndexes.push(i);
|
|
40
|
+
}
|
|
41
|
+
queryInfos.set(jsonPath, {
|
|
42
|
+
jsonPath,
|
|
43
|
+
queryPath,
|
|
44
|
+
schema,
|
|
45
|
+
wildcardIndexes
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
super({
|
|
49
|
+
start(controller) {
|
|
50
|
+
parser = new JSONParserText_default({
|
|
51
|
+
multi,
|
|
52
|
+
onValue: (value, stack) => {
|
|
53
|
+
const path = stackToQueryPath(stack);
|
|
54
|
+
let keep = false;
|
|
55
|
+
for (const { jsonPath, queryPath, schema, wildcardIndexes } of queryInfos.values()) if (queryPath.every((x, j) => isEqual(x, path[j]))) if (path.length === queryPath.length) {
|
|
56
|
+
let valueToEmit;
|
|
57
|
+
if (schema) {
|
|
58
|
+
const result = schema["~standard"].validate(value);
|
|
59
|
+
if (result instanceof Promise) throw new Error("async schema validation is not supported");
|
|
60
|
+
if (result.issues) throw new Error(JSON.stringify(result.issues, null, 2));
|
|
61
|
+
valueToEmit = result.value;
|
|
62
|
+
} else valueToEmit = queryInfos.size === 1 ? value : structuredClone(value);
|
|
63
|
+
console.log("valueToEmit", value, valueToEmit);
|
|
64
|
+
let wildcardKeys;
|
|
65
|
+
if (wildcardIndexes) {
|
|
66
|
+
if (wildcardIndexes) for (const index of wildcardIndexes) {
|
|
67
|
+
const pathComponent = path[index];
|
|
68
|
+
if (pathComponent?.type === "key") {
|
|
69
|
+
if (!wildcardKeys) wildcardKeys = [];
|
|
70
|
+
wildcardKeys.push(pathComponent.value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (wildcardKeys) controller.enqueue({
|
|
75
|
+
jsonPath,
|
|
76
|
+
value: valueToEmit,
|
|
77
|
+
wildcardKeys
|
|
78
|
+
});
|
|
79
|
+
else controller.enqueue({
|
|
80
|
+
jsonPath,
|
|
81
|
+
value: valueToEmit
|
|
82
|
+
});
|
|
83
|
+
} else keep = true;
|
|
84
|
+
else {
|
|
85
|
+
const type = typeof value;
|
|
86
|
+
if (type === "string" || type === "number" || type === "boolean" || value === null) keep = true;
|
|
87
|
+
}
|
|
88
|
+
if (!keep) {
|
|
89
|
+
for (const row of parser.stack) row.value = void 0;
|
|
90
|
+
if (typeof parser.value === "object" && parser.value !== null && parser.key !== void 0) delete parser.value[parser.key];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
transform(chunk) {
|
|
96
|
+
parser.write(chunk);
|
|
97
|
+
},
|
|
98
|
+
flush(controller) {
|
|
99
|
+
parser.checkEnd();
|
|
100
|
+
controller.terminate();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
this._parser = parser;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
//#endregion
|
|
108
|
+
export { createJSONParserStream };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/JSONParserText.d.ts
|
|
2
|
+
type Token = "LEFT_BRACE" | "RIGHT_BRACE" | "LEFT_BRACKET" | "RIGHT_BRACKET" | "COLON" | "COMMA" | "TRUE" | "FALSE" | "NULL" | "STRING" | "NUMBER";
|
|
3
|
+
type ParserState = "VALUE" | "KEY" | "VALUE_AFTER_COMMA" | "KEY_AFTER_COMMA";
|
|
4
|
+
type TokenizerState = "START" | "TRUE1" | "TRUE2" | "TRUE3" | "FALSE1" | "FALSE2" | "FALSE3" | "FALSE4" | "NULL1" | "NULL2" | "NULL3" | "NUMBER-" | "NUMBER0" | "NUMBER" | "STRING1" | "STRING2" | "STRING3" | "STRING4" | "STRING5" | "STRING6";
|
|
5
|
+
type Mode = "OBJECT" | "ARRAY";
|
|
6
|
+
type Key = string | number;
|
|
7
|
+
type Value = any;
|
|
8
|
+
type Stack = {
|
|
9
|
+
key: Key | undefined;
|
|
10
|
+
value: Value | undefined;
|
|
11
|
+
mode: Mode | undefined;
|
|
12
|
+
}[];
|
|
13
|
+
type OnValue = (value: Value, stack: Stack) => void;
|
|
14
|
+
declare class JSONParserText {
|
|
15
|
+
tokenizerState: TokenizerState;
|
|
16
|
+
state: Token | ParserState;
|
|
17
|
+
mode: Mode | undefined;
|
|
18
|
+
stack: Stack;
|
|
19
|
+
string: string | undefined;
|
|
20
|
+
key: Key | undefined;
|
|
21
|
+
value: Value;
|
|
22
|
+
position: number;
|
|
23
|
+
onValue: OnValue;
|
|
24
|
+
unicode: string | undefined;
|
|
25
|
+
highSurrogate: number | undefined;
|
|
26
|
+
seenRootObject: boolean;
|
|
27
|
+
multi: boolean;
|
|
28
|
+
multiIndex: number;
|
|
29
|
+
constructor({
|
|
30
|
+
multi,
|
|
31
|
+
onValue
|
|
32
|
+
}: {
|
|
33
|
+
multi: boolean;
|
|
34
|
+
onValue: OnValue;
|
|
35
|
+
});
|
|
36
|
+
charError(char: string, i: number): void;
|
|
37
|
+
parseError(token: Token, value: Value, i: number): void;
|
|
38
|
+
write(text: string): void;
|
|
39
|
+
push(): void;
|
|
40
|
+
pop(): void;
|
|
41
|
+
emit(value: Value): void;
|
|
42
|
+
onToken(token: Token, value: Value, i: number): void;
|
|
43
|
+
numberReviver(text: string, i: number): void;
|
|
44
|
+
checkEnd(): void;
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { JSONParserText };
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
//#region src/JSONParserText.ts
|
|
2
|
+
const WHITESPACE = new Set([
|
|
3
|
+
" ",
|
|
4
|
+
" ",
|
|
5
|
+
"\n",
|
|
6
|
+
"\r"
|
|
7
|
+
]);
|
|
8
|
+
var JSONParserText = class {
|
|
9
|
+
tokenizerState = "START";
|
|
10
|
+
state = "VALUE";
|
|
11
|
+
mode;
|
|
12
|
+
stack = [];
|
|
13
|
+
string;
|
|
14
|
+
key;
|
|
15
|
+
value;
|
|
16
|
+
position = 0;
|
|
17
|
+
onValue;
|
|
18
|
+
unicode;
|
|
19
|
+
highSurrogate;
|
|
20
|
+
seenRootObject = false;
|
|
21
|
+
multi;
|
|
22
|
+
multiIndex = 0;
|
|
23
|
+
constructor({ multi, onValue }) {
|
|
24
|
+
this.onValue = onValue;
|
|
25
|
+
this.multi = multi;
|
|
26
|
+
}
|
|
27
|
+
charError(char, i) {
|
|
28
|
+
throw new Error(`Unexpected ${JSON.stringify(char)} at position ${this.position + i} in state ${this.tokenizerState}`);
|
|
29
|
+
}
|
|
30
|
+
parseError(token, value, i) {
|
|
31
|
+
throw new Error(`Unexpected ${token}${value ? `(${JSON.stringify(value)})` : ""} at position ${this.position + i} in state ${this.state}`);
|
|
32
|
+
}
|
|
33
|
+
write(text) {
|
|
34
|
+
for (let i = 0, l = text.length; i < l; i++) {
|
|
35
|
+
const n = text[i];
|
|
36
|
+
if (!this.multi && this.stack.length === 0 && this.seenRootObject && !WHITESPACE.has(n)) return this.charError(n, i);
|
|
37
|
+
if (this.multi && this.stack.length === 0 && this.seenRootObject) {}
|
|
38
|
+
if (this.tokenizerState === "START") if (n === "{") this.onToken("LEFT_BRACE", "{", i);
|
|
39
|
+
else if (n === "}") this.onToken("RIGHT_BRACE", "}", i);
|
|
40
|
+
else if (n === "[") this.onToken("LEFT_BRACKET", "[", i);
|
|
41
|
+
else if (n === "]") this.onToken("RIGHT_BRACKET", "]", i);
|
|
42
|
+
else if (n === ":") this.onToken("COLON", ":", i);
|
|
43
|
+
else if (n === ",") this.onToken("COMMA", ",", i);
|
|
44
|
+
else if (n === "t") this.tokenizerState = "TRUE1";
|
|
45
|
+
else if (n === "f") this.tokenizerState = "FALSE1";
|
|
46
|
+
else if (n === "n") this.tokenizerState = "NULL1";
|
|
47
|
+
else if (n === "\"") {
|
|
48
|
+
this.string = "";
|
|
49
|
+
this.tokenizerState = "STRING1";
|
|
50
|
+
} else if (n === "-") {
|
|
51
|
+
this.string = "-";
|
|
52
|
+
this.tokenizerState = "NUMBER-";
|
|
53
|
+
} else if (n === "0") {
|
|
54
|
+
this.string = n;
|
|
55
|
+
this.tokenizerState = "NUMBER0";
|
|
56
|
+
} else if (n >= "1" && n <= "9") {
|
|
57
|
+
this.string = n;
|
|
58
|
+
this.tokenizerState = "NUMBER";
|
|
59
|
+
} else if (WHITESPACE.has(n)) {} else if (n === "␞" && this.multi && this.stack.length === 0) {} else return this.charError(n, i);
|
|
60
|
+
else if (this.tokenizerState === "STRING1") if (n === "\"") {
|
|
61
|
+
this.tokenizerState = "START";
|
|
62
|
+
this.onToken("STRING", this.string, i);
|
|
63
|
+
this.string = void 0;
|
|
64
|
+
} else if (n === "\\") this.tokenizerState = "STRING2";
|
|
65
|
+
else {
|
|
66
|
+
if (n.charCodeAt(0) <= 31) this.charError(n, i);
|
|
67
|
+
this.string += n;
|
|
68
|
+
}
|
|
69
|
+
else if (this.tokenizerState === "STRING2") if (n === "\"") {
|
|
70
|
+
this.string += "\"";
|
|
71
|
+
this.tokenizerState = "STRING1";
|
|
72
|
+
} else if (n === "\\") {
|
|
73
|
+
this.string += "\\";
|
|
74
|
+
this.tokenizerState = "STRING1";
|
|
75
|
+
} else if (n === "/") {
|
|
76
|
+
this.string += "/";
|
|
77
|
+
this.tokenizerState = "STRING1";
|
|
78
|
+
} else if (n === "b") {
|
|
79
|
+
this.string += "\b";
|
|
80
|
+
this.tokenizerState = "STRING1";
|
|
81
|
+
} else if (n === "f") {
|
|
82
|
+
this.string += "\f";
|
|
83
|
+
this.tokenizerState = "STRING1";
|
|
84
|
+
} else if (n === "n") {
|
|
85
|
+
this.string += "\n";
|
|
86
|
+
this.tokenizerState = "STRING1";
|
|
87
|
+
} else if (n === "r") {
|
|
88
|
+
this.string += "\r";
|
|
89
|
+
this.tokenizerState = "STRING1";
|
|
90
|
+
} else if (n === "t") {
|
|
91
|
+
this.string += " ";
|
|
92
|
+
this.tokenizerState = "STRING1";
|
|
93
|
+
} else if (n === "u") {
|
|
94
|
+
this.unicode = "";
|
|
95
|
+
this.tokenizerState = "STRING3";
|
|
96
|
+
} else return this.charError(n, i);
|
|
97
|
+
else if (this.tokenizerState === "STRING3" || this.tokenizerState === "STRING4" || this.tokenizerState === "STRING5" || this.tokenizerState === "STRING6") {
|
|
98
|
+
this.unicode += n;
|
|
99
|
+
if (this.tokenizerState === "STRING3") this.tokenizerState = "STRING4";
|
|
100
|
+
else if (this.tokenizerState === "STRING4") this.tokenizerState = "STRING5";
|
|
101
|
+
else if (this.tokenizerState === "STRING5") this.tokenizerState = "STRING6";
|
|
102
|
+
else if (this.tokenizerState === "STRING6") {
|
|
103
|
+
const intVal = Number.parseInt(this.unicode, 16);
|
|
104
|
+
if (Number.isNaN(intVal)) return this.charError(n, i);
|
|
105
|
+
this.unicode = void 0;
|
|
106
|
+
if (this.highSurrogate !== void 0 && intVal >= 56320 && intVal < 57344) {
|
|
107
|
+
this.string += String.fromCharCode(this.highSurrogate, intVal);
|
|
108
|
+
this.highSurrogate = void 0;
|
|
109
|
+
} else if (this.highSurrogate === void 0 && intVal >= 55296 && intVal < 56320) this.highSurrogate = intVal;
|
|
110
|
+
else {
|
|
111
|
+
if (this.highSurrogate !== void 0) {
|
|
112
|
+
this.string += String.fromCharCode(this.highSurrogate);
|
|
113
|
+
this.highSurrogate = void 0;
|
|
114
|
+
}
|
|
115
|
+
this.string += String.fromCharCode(intVal);
|
|
116
|
+
}
|
|
117
|
+
this.tokenizerState = "STRING1";
|
|
118
|
+
}
|
|
119
|
+
} else if (this.tokenizerState === "NUMBER" || this.tokenizerState === "NUMBER-" || this.tokenizerState === "NUMBER0") {
|
|
120
|
+
if (this.tokenizerState === "NUMBER0" && n >= "0" && n <= "9") return this.charError("0", i - 1);
|
|
121
|
+
switch (n) {
|
|
122
|
+
case "0":
|
|
123
|
+
this.string += n;
|
|
124
|
+
this.tokenizerState = this.tokenizerState === "NUMBER-" ? "NUMBER0" : "NUMBER";
|
|
125
|
+
break;
|
|
126
|
+
case "1":
|
|
127
|
+
case "2":
|
|
128
|
+
case "3":
|
|
129
|
+
case "4":
|
|
130
|
+
case "5":
|
|
131
|
+
case "6":
|
|
132
|
+
case "7":
|
|
133
|
+
case "8":
|
|
134
|
+
case "9":
|
|
135
|
+
case ".":
|
|
136
|
+
case "e":
|
|
137
|
+
case "E":
|
|
138
|
+
case "+":
|
|
139
|
+
case "-":
|
|
140
|
+
this.string += n;
|
|
141
|
+
this.tokenizerState = "NUMBER";
|
|
142
|
+
break;
|
|
143
|
+
default:
|
|
144
|
+
this.tokenizerState = "START";
|
|
145
|
+
this.numberReviver(this.string, i);
|
|
146
|
+
this.string = void 0;
|
|
147
|
+
i--;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
} else if (this.tokenizerState === "TRUE1") if (n === "r") this.tokenizerState = "TRUE2";
|
|
151
|
+
else return this.charError(n, i);
|
|
152
|
+
else if (this.tokenizerState === "TRUE2") if (n === "u") this.tokenizerState = "TRUE3";
|
|
153
|
+
else return this.charError(n, i);
|
|
154
|
+
else if (this.tokenizerState === "TRUE3") if (n === "e") {
|
|
155
|
+
this.tokenizerState = "START";
|
|
156
|
+
this.onToken("TRUE", true, i);
|
|
157
|
+
} else return this.charError(n, i);
|
|
158
|
+
else if (this.tokenizerState === "FALSE1") if (n === "a") this.tokenizerState = "FALSE2";
|
|
159
|
+
else return this.charError(n, i);
|
|
160
|
+
else if (this.tokenizerState === "FALSE2") if (n === "l") this.tokenizerState = "FALSE3";
|
|
161
|
+
else return this.charError(n, i);
|
|
162
|
+
else if (this.tokenizerState === "FALSE3") if (n === "s") this.tokenizerState = "FALSE4";
|
|
163
|
+
else return this.charError(n, i);
|
|
164
|
+
else if (this.tokenizerState === "FALSE4") if (n === "e") {
|
|
165
|
+
this.tokenizerState = "START";
|
|
166
|
+
this.onToken("FALSE", false, i);
|
|
167
|
+
} else return this.charError(n, i);
|
|
168
|
+
else if (this.tokenizerState === "NULL1") if (n === "u") this.tokenizerState = "NULL2";
|
|
169
|
+
else return this.charError(n, i);
|
|
170
|
+
else if (this.tokenizerState === "NULL2") if (n === "l") this.tokenizerState = "NULL3";
|
|
171
|
+
else return this.charError(n, i);
|
|
172
|
+
else if (this.tokenizerState === "NULL3") if (n === "l") {
|
|
173
|
+
this.tokenizerState = "START";
|
|
174
|
+
this.onToken("NULL", null, i);
|
|
175
|
+
} else return this.charError(n, i);
|
|
176
|
+
}
|
|
177
|
+
this.position += text.length;
|
|
178
|
+
}
|
|
179
|
+
push() {
|
|
180
|
+
this.stack.push({
|
|
181
|
+
value: this.value,
|
|
182
|
+
key: this.key,
|
|
183
|
+
mode: this.mode
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
pop() {
|
|
187
|
+
const value = this.value;
|
|
188
|
+
const parent = this.stack.pop();
|
|
189
|
+
this.value = parent.value;
|
|
190
|
+
this.key = parent.key;
|
|
191
|
+
this.mode = parent.mode;
|
|
192
|
+
this.emit(value);
|
|
193
|
+
if (!this.mode) this.state = "VALUE";
|
|
194
|
+
}
|
|
195
|
+
emit(value) {
|
|
196
|
+
if (this.mode) this.state = "COMMA";
|
|
197
|
+
this.onValue(value, [...this.stack, {
|
|
198
|
+
value: this.value,
|
|
199
|
+
key: this.key,
|
|
200
|
+
mode: this.mode
|
|
201
|
+
}]);
|
|
202
|
+
}
|
|
203
|
+
onToken(token, value, i) {
|
|
204
|
+
if (this.stack.length === 0) {
|
|
205
|
+
if (!this.seenRootObject) this.seenRootObject = true;
|
|
206
|
+
}
|
|
207
|
+
if (this.state === "VALUE" || this.state === "VALUE_AFTER_COMMA") if (token === "STRING" || token === "NUMBER" || token === "TRUE" || token === "FALSE" || token === "NULL") {
|
|
208
|
+
if (this.value) this.value[this.key] = value;
|
|
209
|
+
this.emit(value);
|
|
210
|
+
} else if (token === "LEFT_BRACE") {
|
|
211
|
+
this.push();
|
|
212
|
+
if (this.value) this.value = this.value[this.key] = {};
|
|
213
|
+
else this.value = {};
|
|
214
|
+
this.key = void 0;
|
|
215
|
+
this.state = "KEY";
|
|
216
|
+
this.mode = "OBJECT";
|
|
217
|
+
} else if (token === "LEFT_BRACKET") {
|
|
218
|
+
this.push();
|
|
219
|
+
if (this.value) this.value = this.value[this.key] = [];
|
|
220
|
+
else this.value = [];
|
|
221
|
+
this.key = 0;
|
|
222
|
+
this.mode = "ARRAY";
|
|
223
|
+
this.state = "VALUE";
|
|
224
|
+
} else if (token === "RIGHT_BRACE") if (this.mode === "OBJECT" && this.state !== "VALUE_AFTER_COMMA") this.pop();
|
|
225
|
+
else return this.parseError(token, value, i);
|
|
226
|
+
else if (token === "RIGHT_BRACKET") if (this.mode === "ARRAY" && this.state !== "VALUE_AFTER_COMMA") this.pop();
|
|
227
|
+
else return this.parseError(token, value, i);
|
|
228
|
+
else return this.parseError(token, value, i);
|
|
229
|
+
else if (this.state === "KEY" || this.state === "KEY_AFTER_COMMA") if (token === "STRING") {
|
|
230
|
+
this.key = value;
|
|
231
|
+
this.state = "COLON";
|
|
232
|
+
} else if (token === "RIGHT_BRACE" && this.state !== "KEY_AFTER_COMMA") this.pop();
|
|
233
|
+
else return this.parseError(token, value, i);
|
|
234
|
+
else if (this.state === "COLON") if (token === "COLON") this.state = "VALUE";
|
|
235
|
+
else return this.parseError(token, value, i);
|
|
236
|
+
else if (this.state === "COMMA") if (token === "COMMA") {
|
|
237
|
+
if (this.mode === "ARRAY") {
|
|
238
|
+
this.key++;
|
|
239
|
+
this.state = "VALUE_AFTER_COMMA";
|
|
240
|
+
} else if (this.mode === "OBJECT") this.state = "KEY_AFTER_COMMA";
|
|
241
|
+
} else if (token === "RIGHT_BRACKET" && this.mode === "ARRAY" || token === "RIGHT_BRACE" && this.mode === "OBJECT") this.pop();
|
|
242
|
+
else return this.parseError(token, value, i);
|
|
243
|
+
else return this.parseError(token, value, i);
|
|
244
|
+
}
|
|
245
|
+
numberReviver(text, i) {
|
|
246
|
+
const number = JSON.parse(text);
|
|
247
|
+
if (Number.isNaN(number)) return this.charError(text, i);
|
|
248
|
+
this.onToken("NUMBER", number, i);
|
|
249
|
+
}
|
|
250
|
+
checkEnd() {
|
|
251
|
+
if (this.stack.length > 0) throw new Error(`Unexpected end of input at position ${this.position} in state ${this.state}`);
|
|
252
|
+
if (this.state === "VALUE" && this.tokenizerState === "NUMBER" && this.string !== void 0) {
|
|
253
|
+
this.numberReviver(this.string, this.position - 1);
|
|
254
|
+
this.string = void 0;
|
|
255
|
+
} else if (!this.seenRootObject) throw new Error("No data in input");
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
var JSONParserText_default = JSONParserText;
|
|
259
|
+
|
|
260
|
+
//#endregion
|
|
261
|
+
export { JSONParserText_default as default };
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import parser from "jsonpath-rfc9535/parser";
|
|
2
|
+
|
|
3
|
+
//#region src/jsonPathToQueryPath.ts
|
|
4
|
+
const jsonPathToQueryPath = (jsonPath) => {
|
|
5
|
+
let parsed;
|
|
6
|
+
try {
|
|
7
|
+
parsed = parser(jsonPath);
|
|
8
|
+
} catch (error) {
|
|
9
|
+
throw new Error(`Error parsing JSONPath "${jsonPath}"`, { cause: error });
|
|
10
|
+
}
|
|
11
|
+
return parsed.segments.flatMap((segment) => {
|
|
12
|
+
if (segment.type === "ChildSegment") {
|
|
13
|
+
const node = segment.node;
|
|
14
|
+
if (node.type === "MemberNameShorthand") return {
|
|
15
|
+
type: "key",
|
|
16
|
+
value: node.value
|
|
17
|
+
};
|
|
18
|
+
else if (node.type === "BracketedSelection") return node.selectors.map((selector) => {
|
|
19
|
+
if (selector.type === "NameSelector") return {
|
|
20
|
+
type: "key",
|
|
21
|
+
value: selector.value
|
|
22
|
+
};
|
|
23
|
+
else if (selector.type === "WildcardSelector") return { type: "wildcard" };
|
|
24
|
+
else throw new Error(`Unsupported node: ${JSON.stringify(node)}`);
|
|
25
|
+
});
|
|
26
|
+
else if (node.type === "WildcardSelector") return { type: "wildcard" };
|
|
27
|
+
else throw new Error(`${segment.type} node type not supported`);
|
|
28
|
+
} else throw new Error(`${segment.type} segment type not supported`);
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
export { jsonPathToQueryPath };
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "json-web-streams",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Streaming JSON parser built on top of the Web Streams API, so it works in web browsers, Node.js, and many other environments",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"web streams",
|
|
7
|
+
"TransformStream",
|
|
8
|
+
"JSON"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/zengm-games/json-web-streams",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git://github.com/zengm-games/json-web-streams.git"
|
|
14
|
+
},
|
|
15
|
+
"license": "Apache-2.0",
|
|
16
|
+
"author": "Jeremy Scheff <jeremy@zengm.com> (https://zengm.com/)",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "dist/index.js",
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsdown --unbundle",
|
|
25
|
+
"format": "prettier --write .",
|
|
26
|
+
"lint": "npm-run-all --parallel lint:oxlint lint:tsc",
|
|
27
|
+
"lint:oxlint": "oxlint --type-aware",
|
|
28
|
+
"lint:tsc": "tsc",
|
|
29
|
+
"prepare": "husky",
|
|
30
|
+
"test": "vitest"
|
|
31
|
+
},
|
|
32
|
+
"lint-staged": {
|
|
33
|
+
"*.{js,cjs,mjs,jsx,json,scss,ts,cts,mts,tsx,md}": "prettier --write"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@standard-schema/spec": "^1.0.0",
|
|
37
|
+
"jsonpath-rfc9535": "^1.3.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
|
|
41
|
+
"@types/node": "^24.8.1",
|
|
42
|
+
"husky": "^9.1.7",
|
|
43
|
+
"lint-staged": "^16.2.4",
|
|
44
|
+
"npm-run-all2": "^8.0.4",
|
|
45
|
+
"oxlint": "^1.23.0",
|
|
46
|
+
"oxlint-tsgolint": "^0.2.0",
|
|
47
|
+
"prettier": "^3.6.2",
|
|
48
|
+
"prettier-plugin-packagejson": "^2.5.19",
|
|
49
|
+
"tsdown": "^0.15.7",
|
|
50
|
+
"typescript": "^5.9.3",
|
|
51
|
+
"vitest": "^3.2.4",
|
|
52
|
+
"zod": "^4.1.12"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=22",
|
|
56
|
+
"pnpm": "^10.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|