json-web-streams
Version:
Streaming JSON parser built on top of the Web Streams API, so it works in web browsers, Node.js, and many other environments
142 lines (140 loc) • 4.95 kB
JavaScript
import { JSONParseStreamRaw } from "./JSONParseStreamRaw.js";
import { jsonPathToPathArray } from "./jsonPathToPathArray.js";
//#region src/JSONParseStream.ts
const isEqual = (x, y) => {
if (!y) return false;
if (x.type === "wildcard") {
if (y.mode === "ARRAY") return true;
else if (y.mode === "OBJECT") return true;
} else if (y.mode === "OBJECT" && x.value === y.key) return true;
return false;
};
var JSONParseStream = class extends TransformStream {
_parser;
constructor(jsonPaths, options) {
let parser;
let minPathArrayLength = Infinity;
let maxPathArrayLength = -Infinity;
const jsonPathInfos = jsonPaths.map((row) => {
let key;
let path;
let schema;
if (typeof row === "string") path = row;
else {
key = row.key;
path = row.path;
schema = row.schema;
}
const pathArray = jsonPathToPathArray(path);
let wildcardIndexes;
for (const [i, component] of pathArray.entries()) if (component.type === "wildcard") {
if (wildcardIndexes === void 0) wildcardIndexes = [];
wildcardIndexes.push(i);
}
const matches = pathArray.length === 0 ? "yes" : "unknown";
if (pathArray.length > maxPathArrayLength) maxPathArrayLength = pathArray.length;
if (pathArray.length < minPathArrayLength) minPathArrayLength = pathArray.length;
return {
key,
matches,
path,
pathArray,
validate: schema?.["~standard"].validate,
wildcardIndexes
};
});
const jsonPathInfosThatMatch = new Set(jsonPathInfos.filter((info) => info.matches === "yes"));
const updateMatches = (type) => {
for (const info of jsonPathInfos) {
const pathArray = info.pathArray;
if ((info.matches === "unknown" && type === "push" || info.matches !== "unknown" && info.matches !== "noBeforeEnd" && type === "key") && parser.stack.length === pathArray.length) {
let pathMatches = "yes";
for (let j = 0; j < pathArray.length; j++) {
let stackComponent = parser.stack[j + 1];
if (!stackComponent) {
if (type === "key") stackComponent = parser;
else if (pathArray[j].type === "wildcard") {
pathMatches = "yes";
break;
}
}
if (!isEqual(pathArray[j], stackComponent)) {
if (j < pathArray.length - 1) pathMatches = "noBeforeEnd";
else pathMatches = "noAtEnd";
break;
}
}
info.matches = pathMatches;
if (pathMatches === "yes") jsonPathInfosThatMatch.add(info);
else jsonPathInfosThatMatch.delete(info);
}
}
};
super({
start(controller) {
parser = new JSONParseStreamRaw({
multi: options?.multi,
onKey: (stackLength) => {
if (stackLength <= maxPathArrayLength && stackLength >= minPathArrayLength) updateMatches("key");
},
onPop: (stackLength) => {
if (stackLength < maxPathArrayLength && stackLength >= minPathArrayLength - 1) {
for (const info of jsonPathInfos) if (info.matches !== "unknown" && stackLength < info.pathArray.length) {
info.matches = "unknown";
jsonPathInfosThatMatch.delete(info);
}
}
},
onPush: (stackLength) => {
if (stackLength <= maxPathArrayLength && stackLength >= minPathArrayLength) updateMatches("push");
},
onValue: (value) => {
let keep = false;
for (const { key, path, pathArray, validate, wildcardIndexes } of jsonPathInfosThatMatch) if (parser.stack.length === pathArray.length) {
let valueToEmit;
if (validate) {
const result = validate(value);
if (result instanceof Promise) throw new TypeError("Schema validation must be synchronous");
if (result.issues) throw new Error(JSON.stringify(result.issues, null, 2));
valueToEmit = result.value;
} else valueToEmit = value;
let wildcardKeys;
if (wildcardIndexes) for (const index of wildcardIndexes) {
const stackComponent = parser.stack[index + 1] ?? parser;
if (stackComponent.mode === "OBJECT" && stackComponent.key !== void 0) {
if (!wildcardKeys) wildcardKeys = [];
wildcardKeys.push(stackComponent.key);
}
}
if (wildcardKeys) controller.enqueue({
key: key ?? path,
value: valueToEmit,
wildcardKeys
});
else controller.enqueue({
key: key ?? path,
value: valueToEmit
});
} else keep = true;
if (keep) return;
const type = typeof value;
if (!(type === "string" || type === "number" || type === "boolean" || value === null)) {
for (const row of parser.stack) row.value = void 0;
if (typeof parser.value === "object" && parser.value !== null && parser.key !== void 0) parser.value[parser.key] = void 0;
}
}
});
},
transform(chunk) {
parser.write(chunk);
},
flush(controller) {
parser.checkEnd();
controller.terminate();
}
});
this._parser = parser;
}
};
//#endregion
export { JSONParseStream };