diff --git a/README.md b/README.md index e9ff092..e78ac01 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,23 @@ An object that specifies what kind of partialness is allowed during JSON parsing - `ATOM`: Allow all atomic values. - `COLLECTION`: Allow all collection values. - `ALL`: Allow all values. +- `OUTERMOST_OBJ`: Allow partial parsing of the outermost object. +- `OUTERMOST_ARR`: Allow partial parsing of the outermost array. + +#### OUTERMOST_OBJ and OUTERMOST_ARR + +These new allowances provide more granular control over parsing partial JSON: + +- `OUTERMOST_OBJ`: Allows partial parsing of the outermost object in the JSON hierarchy, even if `OBJ` is not allowed. +- `OUTERMOST_ARR`: Allows partial parsing of the outermost array in the JSON hierarchy, even if `ARR` is not allowed. + +The "outermost" object or array doesn't necessarily mean the root of the JSON. It refers to the highest-level object or array in the current parsing context, which doesn't have another object or array above it in the JSON hierarchy. + +These allowances are particularly useful when you want to parse partial data at the highest level of your JSON structure while maintaining stricter parsing for nested objects or arrays. + +For examples of how to use these allowances, please refer to the test cases in the project repository. + +These new allowances provide more fine-grained control over partial JSON parsing, especially useful in scenarios where you want to be more permissive with top-level structures while maintaining stricter parsing for nested elements. ## Testing diff --git a/src/index.ts b/src/index.ts index 976a72c..455e5cb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ import { Allow } from "./options"; export * from "./options"; -class PartialJSON extends Error { } +class PartialJSON extends Error {} -class MalformedJSON extends Error { } +class MalformedJSON extends Error {} /** * Parse incomplete JSON @@ -14,174 +14,328 @@ class MalformedJSON extends Error { } * @throws {MalformedJSON} If the JSON is malformed */ function parseJSON(jsonString: string, allowPartial: number = Allow.ALL): any { - if (typeof jsonString !== "string") { - throw new TypeError(`expecting str, got ${typeof jsonString}`); - } - if (!jsonString.trim()) { - throw new Error(`${jsonString} is empty`); - } - return _parseJSON(jsonString.trim(), allowPartial); -}; + if (typeof jsonString !== "string") { + throw new TypeError(`expecting string, got ${typeof jsonString}`); + } + if (!jsonString.trim()) { + throw new Error(`Input is empty`); + } + return _parseJSON(jsonString.trim(), allowPartial); +} const _parseJSON = (jsonString: string, allow: number) => { - const length = jsonString.length; - let index = 0; + const length = jsonString.length; + let index = 0; + let objectDepth = 0; // Track the current depth of objects + let arrayDepth = 0; // Track the current depth of arrays - const markPartialJSON = (msg: string) => { - throw new PartialJSON(`${msg} at position ${index}`); - }; + const markPartialJSON = (msg: string) => { + throw new PartialJSON(`${msg} at position ${index}`); + }; - const throwMalformedError = (msg: string) => { - throw new MalformedJSON(`${msg} at position ${index}`); - }; + const throwMalformedError = (msg: string) => { + throw new MalformedJSON(`${msg} at position ${index}`); + }; - const parseAny: () => any = () => { - skipBlank(); - if (index >= length) markPartialJSON("Unexpected end of input"); - if (jsonString[index] === '"') return parseStr(); - if (jsonString[index] === "{") return parseObj(); - if (jsonString[index] === "[") return parseArr(); - if (jsonString.substring(index, index + 4) === "null" || (Allow.NULL & allow && length - index < 4 && "null".startsWith(jsonString.substring(index)))) { - index += 4; - return null; - } - if (jsonString.substring(index, index + 4) === "true" || (Allow.BOOL & allow && length - index < 4 && "true".startsWith(jsonString.substring(index)))) { - index += 4; - return true; - } - if (jsonString.substring(index, index + 5) === "false" || (Allow.BOOL & allow && length - index < 5 && "false".startsWith(jsonString.substring(index)))) { - index += 5; - return false; - } - if (jsonString.substring(index, index + 8) === "Infinity" || (Allow.INFINITY & allow && length - index < 8 && "Infinity".startsWith(jsonString.substring(index)))) { - index += 8; - return Infinity; - } - if (jsonString.substring(index, index + 9) === "-Infinity" || (Allow._INFINITY & allow && 1 < length - index && length - index < 9 && "-Infinity".startsWith(jsonString.substring(index)))) { - index += 9; - return -Infinity; + const parseAny: () => any = () => { + skipBlank(); + if (index >= length) markPartialJSON("Unexpected end of input"); + const currentChar = jsonString[index]; + + // Handle string + if (currentChar === '"') return parseStr(); + + // Handle object + if (currentChar === "{") { + objectDepth++; + const result = parseObj(); + objectDepth--; + return result; + } + + // Handle array + if (currentChar === "[") { + arrayDepth++; + const result = parseArr(); + arrayDepth--; + return result; + } + + // Handle literals and numbers + if ( + jsonString.substring(index, index + 4) === "null" || + (Allow.NULL & allow && + length - index < 4 && + "null".startsWith(jsonString.substring(index))) + ) { + index += 4; + return null; + } + if ( + jsonString.substring(index, index + 4) === "true" || + (Allow.BOOL & allow && + length - index < 4 && + "true".startsWith(jsonString.substring(index))) + ) { + index += 4; + return true; + } + if ( + jsonString.substring(index, index + 5) === "false" || + (Allow.BOOL & allow && + length - index < 5 && + "false".startsWith(jsonString.substring(index))) + ) { + index += 5; + return false; + } + if ( + jsonString.substring(index, index + 8) === "Infinity" || + (Allow.INFINITY & allow && + length - index < 8 && + "Infinity".startsWith(jsonString.substring(index))) + ) { + index += 8; + return Infinity; + } + if ( + jsonString.substring(index, index + 9) === "-Infinity" || + (Allow._INFINITY & allow && + 1 < length - index && + length - index < 9 && + "-Infinity".startsWith(jsonString.substring(index))) + ) { + index += 9; + return -Infinity; + } + if ( + jsonString.substring(index, index + 3) === "NaN" || + (Allow.NAN & allow && + length - index < 3 && + "NaN".startsWith(jsonString.substring(index))) + ) { + index += 3; + return NaN; + } + return parseNum(); + }; + + const parseStr: () => string = () => { + const start = index; + let escape = false; + index++; // skip initial quote + while (index < length) { + const char = jsonString[index]; + if (char === '"' && !escape) { + index++; // include the closing quote + try { + return JSON.parse(jsonString.substring(start, index)); + } catch (e) { + throwMalformedError(`Invalid string: ${e}`); } - if (jsonString.substring(index, index + 3) === "NaN" || (Allow.NAN & allow && length - index < 3 && "NaN".startsWith(jsonString.substring(index)))) { - index += 3; - return NaN; + } + if (char === "\\" && !escape) { + escape = true; + } else { + escape = false; + } + index++; + } + // If we reach here, the string was unterminated + if (Allow.STR & allow) { + try { + // Attempt to close the string by adding the closing quote + return JSON.parse(jsonString.substring(start, index) + '"'); + } catch (e) { + // Attempt to recover by removing trailing backslashes + const lastBackslash = jsonString.lastIndexOf("\\"); + if (lastBackslash > start) { + try { + return JSON.parse(jsonString.substring(start, lastBackslash) + '"'); + } catch (_) {} } - return parseNum(); - }; - - const parseStr: () => string = () => { - const start = index; - let escape = false; - index++; // skip initial quote - while (index < length && (jsonString[index] !== '"' || (escape && jsonString[index - 1] === "\\"))) { - escape = jsonString[index] === "\\" ? !escape : false; - index++; + throwMalformedError("Unterminated string literal"); + } + } + markPartialJSON("Unterminated string literal"); + }; + + const parseObj = () => { + const isOutermost = objectDepth === 1; + index++; // skip initial brace + skipBlank(); + const obj: Record = {}; + try { + while (jsonString[index] !== "}") { + skipBlank(); + if (index >= length) { + if ( + (isOutermost && allow & Allow.OUTERMOST_OBJ) || + allow & Allow.OBJ + ) { + return obj; + } + markPartialJSON("Unexpected end of object"); } - if (jsonString.charAt(index) == '"') { - try { - return JSON.parse(jsonString.substring(start, ++index - Number(escape))); - } catch (e) { - throwMalformedError(String(e)); - } - } else if (Allow.STR & allow) { - try { - return JSON.parse(jsonString.substring(start, index - Number(escape)) + '"'); - } catch (e) { - // SyntaxError: Invalid escape sequence - return JSON.parse(jsonString.substring(start, jsonString.lastIndexOf("\\")) + '"'); - } + // Parse key + const key = parseStr(); + skipBlank(); + if (jsonString[index] !== ":") { + throwMalformedError(`Expected ':' after key "${key}"`); } - markPartialJSON("Unterminated string literal"); - }; - - const parseObj = () => { - index++; // skip initial brace + index++; // skip colon skipBlank(); - const obj: Record = {}; + // Parse value try { - while (jsonString[index] !== "}") { - skipBlank(); - if (index >= length && Allow.OBJ & allow) return obj; - const key = parseStr(); - skipBlank(); - index++; // skip colon - try { - const value = parseAny(); - obj[key] = value; - } catch (e) { - if (Allow.OBJ & allow) return obj; - else throw e; - } - skipBlank(); - if (jsonString[index] === ",") index++; // skip comma - } + const value = parseAny(); + obj[key] = value; } catch (e) { - if (Allow.OBJ & allow) return obj; - else markPartialJSON("Expected '}' at end of object"); + if ( + (isOutermost && allow & Allow.OUTERMOST_OBJ) || + allow & Allow.OBJ + ) { + return obj; + } + throw e; } - index++; // skip final brace + skipBlank(); + // Handle comma or end of object + if (jsonString[index] === ",") { + index++; // skip comma + skipBlank(); + // If next character is '}', it's the end of the object + if (jsonString[index] === "}") { + break; + } + } + } + } catch (e) { + if ((isOutermost && allow & Allow.OUTERMOST_OBJ) || allow & Allow.OBJ) { return obj; - }; + } else { + markPartialJSON("Expected '}' at end of object"); + } + } + if (jsonString[index] === "}") { + index++; // skip final brace + return obj; + } + // If we reach here, the object was not properly closed + if ((isOutermost && allow & Allow.OUTERMOST_OBJ) || allow & Allow.OBJ) { + return obj; + } + markPartialJSON("Expected '}' at end of object"); + }; - const parseArr = () => { - index++; // skip initial bracket - const arr = []; - try { - while (jsonString[index] !== "]") { - arr.push(parseAny()); - skipBlank(); - if (jsonString[index] === ",") { - index++; // skip comma - } - } - } catch (e) { - if (Allow.ARR & allow) { - return arr; - } - markPartialJSON("Expected ']' at end of array"); + const parseArr = () => { + const isOutermost = arrayDepth === 1; + index++; // skip initial bracket + const arr: any[] = []; + try { + while (jsonString[index] !== "]") { + skipBlank(); + if (index >= length) { + if ( + (isOutermost && allow & Allow.OUTERMOST_ARR) || + allow & Allow.ARR + ) { + return arr; + } + markPartialJSON("Unexpected end of array"); } - index++; // skip final bracket - return arr; - }; - - const parseNum = () => { - if (index === 0) { - if (jsonString === "-") throwMalformedError("Not sure what '-' is"); - try { - return JSON.parse(jsonString); - } catch (e) { - if (Allow.NUM & allow) - try { - return JSON.parse(jsonString.substring(0, jsonString.lastIndexOf("e"))); - } catch (e) { } - throwMalformedError(String(e)); - } + // Parse value + const value = parseAny(); + arr.push(value); + skipBlank(); + // Handle comma or end of array + if (jsonString[index] === ",") { + index++; // skip comma + skipBlank(); + // If next character is ']', it's the end of the array + if (jsonString[index] === "]") { + break; + } } + } + } catch (e) { + if ((isOutermost && allow & Allow.OUTERMOST_ARR) || allow & Allow.ARR) { + return arr; + } + throw e; + } + if (jsonString[index] === "]") { + index++; // skip final bracket + return arr; + } + // If we reach here, the array was not properly closed + if ((isOutermost && allow & Allow.OUTERMOST_ARR) || allow & Allow.ARR) { + return arr; + } + markPartialJSON("Expected ']' at end of array"); + }; - const start = index; + const parseNum = () => { + const start = index; - if (jsonString[index] === "-") index++; - while (jsonString[index] && ",]}".indexOf(jsonString[index]) === -1) index++; + // Handle negative sign + if (jsonString[index] === "-") index++; - if (index == length && !(Allow.NUM & allow)) markPartialJSON("Unterminated number literal"); + // Integral part + while (index < length && /[0-9]/.test(jsonString[index])) { + index++; + } - try { - return JSON.parse(jsonString.substring(start, index)); - } catch (e) { - if (jsonString.substring(start, index) === "-") markPartialJSON("Not sure what '-' is"); - try { - return JSON.parse(jsonString.substring(start, jsonString.lastIndexOf("e"))); - } catch (e) { - throwMalformedError(String(e)); - } - } - }; + // Fractional part + if (jsonString[index] === ".") { + index++; + while (index < length && /[0-9]/.test(jsonString[index])) { + index++; + } + } + + // Exponent part + if (jsonString[index] === "e" || jsonString[index] === "E") { + index++; + if (jsonString[index] === "+" || jsonString[index] === "-") { + index++; + } + while (index < length && /[0-9]/.test(jsonString[index])) { + index++; + } + } - const skipBlank = () => { - while (index < length && " \n\r\t".includes(jsonString[index])) { - index++; + const numStr = jsonString.substring(start, index); + + try { + return JSON.parse(numStr); + } catch (e) { + if (Allow.NUM & allow) { + // Attempt to parse the valid part of the number + const validMatch = numStr.match(/^-?\d+(\.\d+)?([eE][+-]?\d+)?/); + if (validMatch && validMatch[0]) { + try { + return JSON.parse(validMatch[0]); + } catch (_) {} } - }; - return parseAny(); + } + throwMalformedError(`Invalid number '${numStr}'`); + } + }; + + const skipBlank = () => { + while (index < length && " \n\r\t".includes(jsonString[index])) { + index++; + } + }; + + const result = parseAny(); + skipBlank(); + + if (index < length) { + throwMalformedError(`Unexpected token '${jsonString[index]}'`); + } + + return result; }; const parse = parseJSON; diff --git a/src/options.ts b/src/options.ts index 5668d51..eecf468 100644 --- a/src/options.ts +++ b/src/options.ts @@ -6,59 +6,69 @@ */ /** - * allow partial strings like `"hello \u12` to be parsed as `"hello "` + * Allow partial strings like `"hello \u12` to be parsed as `"hello "` */ -export const STR = 0b000000001; +export const STR = 0b000000001; // 1 /** - * allow partial numbers like `123.` to be parsed as `123` + * Allow partial numbers like `123.` to be parsed as `123` */ -export const NUM = 0b000000010; +export const NUM = 0b000000010; // 2 /** - * allow partial arrays like `[1, 2,` to be parsed as `[1, 2]` + * Allow partial arrays like `[1, 2,` to be parsed as `[1, 2]` */ -export const ARR = 0b000000100; +export const ARR = 0b000000100; // 4 /** - * allow partial objects like `{"a": 1, "b":` to be parsed as `{"a": 1}` + * Allow partial objects like `{"a": 1, "b":` to be parsed as `{"a": 1}` */ -export const OBJ = 0b000001000; +export const OBJ = 0b000001000; // 8 /** - * allow `nu` to be parsed as `null` + * Allow `nu` to be parsed as `null` */ -export const NULL = 0b000010000; +export const NULL = 0b000010000; // 16 /** - * allow `tr` to be parsed as `true`, and `fa` to be parsed as `false` + * Allow `tr` to be parsed as `true`, and `fa` to be parsed as `false` */ -export const BOOL = 0b000100000; +export const BOOL = 0b000100000; // 32 /** - * allow `Na` to be parsed as `NaN` + * Allow `Na` to be parsed as `NaN` */ -export const NAN = 0b001000000; +export const NAN = 0b001000000; // 64 /** - * allow `Inf` to be parsed as `Infinity` + * Allow `Inf` to be parsed as `Infinity` */ -export const INFINITY = 0b010000000; +export const INFINITY = 0b010000000; // 128 /** - * allow `-Inf` to be parsed as `-Infinity` + * Allow `-Inf` to be parsed as `-Infinity` */ -export const _INFINITY = 0b100000000; +export const _INFINITY = 0b100000000; // 256 -export const INF = INFINITY | _INFINITY; -export const SPECIAL = NULL | BOOL | INF | NAN; -export const ATOM = STR | NUM | SPECIAL; -export const COLLECTION = ARR | OBJ; -export const ALL = ATOM | COLLECTION; +/** + * Allow partial parsing of the outermost JSON object + */ +export const OUTERMOST_OBJ = 0b0000000100000000; // 512 -/** +/** + * Allow partial parsing of the outermost JSON array + */ +export const OUTERMOST_ARR = 0b0000001000000000; // 1024 + +export const INF = INFINITY | _INFINITY; // 384 +export const SPECIAL = NULL | BOOL | INF | NAN; // 432 +export const ATOM = STR | NUM | SPECIAL; // 499 +export const COLLECTION = ARR | OBJ; // 12 +export const ALL = ATOM | COLLECTION; // 511 + +/** * Control what types you allow to be partially parsed. - * The default is to allow all types to be partially parsed, which in most casees is the best option. + * The default is to allow all types to be partially parsed, which in most cases is the best option. * @example * If you don't want to allow partial objects, you can use the following code: * ```ts @@ -76,6 +86,23 @@ export const ALL = ATOM | COLLECTION; * parse(`["complete string", "incompl`, ~Allow.STR); // [ 'complete string' ] * ``` */ -export const Allow = { STR, NUM, ARR, OBJ, NULL, BOOL, NAN, INFINITY, _INFINITY, INF, SPECIAL, ATOM, COLLECTION, ALL }; +export const Allow = { + STR, + NUM, + ARR, + OBJ, + NULL, + BOOL, + NAN, + INFINITY, + _INFINITY, + OUTERMOST_OBJ, + OUTERMOST_ARR, + INF, + SPECIAL, + ATOM, + COLLECTION, + ALL, +}; export default Allow; diff --git a/tests/examples.test.js b/tests/examples.test.js index 86c7c02..fca8aad 100644 --- a/tests/examples.test.js +++ b/tests/examples.test.js @@ -1,62 +1,133 @@ import { parse, PartialJSON, MalformedJSON } from "../src/index"; -import { STR, NUM, ARR, OBJ, NULL, BOOL, NAN, INFINITY, _INFINITY } from "../src/options"; +import { + STR, + NUM, + ARR, + OBJ, + NULL, + BOOL, + NAN, + INFINITY, + _INFINITY, + OUTERMOST_OBJ, + OUTERMOST_ARR, +} from "../src/options"; import { test, expect } from "vitest"; test("str", () => { - expect(parse('"', STR)).toBe(""); - expect(parse('" \\x12', STR)).toBe(" "); - expect(() => parse('"', ~STR)).toThrow(PartialJSON); + expect(parse('"', STR)).toBe(""); + expect(parse('" \\x12', STR)).toBe(" "); + expect(() => parse('"', ~STR)).toThrow(PartialJSON); }); test("arr", () => { - expect(parse('["', ARR)).toEqual([]); - expect(parse('["', ARR | STR)).toEqual([""]); + expect(parse('["', ARR)).toEqual([]); + expect(parse('["', ARR | STR)).toEqual([""]); - expect(() => parse("[", STR)).toThrow(PartialJSON); - expect(() => parse('["', STR)).toThrow(PartialJSON); - expect(() => parse('[""', STR)).toThrow(PartialJSON); - expect(() => parse('["",', STR)).toThrow(PartialJSON); + expect(() => parse("[", STR)).toThrow(PartialJSON); + expect(() => parse('["', STR)).toThrow(PartialJSON); + expect(() => parse('[""', STR)).toThrow(PartialJSON); + expect(() => parse('["",', STR)).toThrow(PartialJSON); }); test("obj", () => { - expect(parse('{"": "', OBJ)).toEqual({}); - expect(parse('{"": "', OBJ | STR)).toEqual({ "": "" }); + expect(parse('{"": "', OBJ)).toEqual({}); + expect(parse('{"": "', OBJ | STR)).toEqual({ "": "" }); - expect(() => parse("{", STR)).toThrow(PartialJSON); - expect(() => parse('{"', STR)).toThrow(PartialJSON); - expect(() => parse('{""', STR)).toThrow(PartialJSON); - expect(() => parse('{"":', STR)).toThrow(PartialJSON); - expect(() => parse('{"":"', STR)).toThrow(PartialJSON); - expect(() => parse('{"":""', STR)).toThrow(PartialJSON); + expect(() => parse("{", STR)).toThrow(PartialJSON); + expect(() => parse('{"', STR)).toThrow(PartialJSON); + expect(() => parse('{""', STR)).toThrow(PartialJSON); + expect(() => parse('{"":', STR)).toThrow(PartialJSON); + expect(() => parse('{"":"', STR)).toThrow(PartialJSON); + expect(() => parse('{"":""', STR)).toThrow(PartialJSON); }); test("singletons", () => { - expect(parse("n", NULL)).toBe(null); - expect(() => parse("n", ~NULL)).toThrow(MalformedJSON); + expect(parse("n", NULL)).toBe(null); + expect(() => parse("n", ~NULL)).toThrow(MalformedJSON); - expect(parse("t", BOOL)).toBe(true); - expect(() => parse("t", ~BOOL)).toThrow(MalformedJSON); + expect(parse("t", BOOL)).toBe(true); + expect(() => parse("t", ~BOOL)).toThrow(MalformedJSON); - expect(parse("f", BOOL)).toBe(false); - expect(() => parse("f", ~BOOL)).toThrow(MalformedJSON); + expect(parse("f", BOOL)).toBe(false); + expect(() => parse("f", ~BOOL)).toThrow(MalformedJSON); - expect(parse("I", INFINITY)).toBe(Infinity); - expect(() => parse("I", ~INFINITY)).toThrow(MalformedJSON); + expect(parse("I", INFINITY)).toBe(Infinity); + expect(() => parse("I", ~INFINITY)).toThrow(MalformedJSON); - expect(parse("-I", _INFINITY)).toBe(-Infinity); - expect(() => parse("-I", ~_INFINITY)).toThrow(MalformedJSON); + expect(parse("-I", _INFINITY)).toBe(-Infinity); + expect(() => parse("-I", ~_INFINITY)).toThrow(MalformedJSON); - expect(Number.isNaN(parse("N", NAN))).toBe(true); - expect(() => parse("N", ~NAN)).toThrow(MalformedJSON); + expect(Number.isNaN(parse("N", NAN))).toBe(true); + expect(() => parse("N", ~NAN)).toThrow(MalformedJSON); }); test("num", () => { - expect(parse("0", ~NUM)).toBe(0); - expect(parse("-1.25e+4", ~NUM)).toBe(-1.25e4); - expect(parse("-1.25e+", NUM)).toBe(-1.25); - expect(parse("-1.25e", NUM)).toBe(-1.25); + expect(parse("0", ~NUM)).toBe(0); + expect(parse("-1.25e+4", ~NUM)).toBe(-1.25e4); + expect(parse("-1.25e+", NUM)).toBe(-1.25); + expect(parse("-1.25e", NUM)).toBe(-1.25); }); test("require", () => { - expect(require("partial-json").STR).toBe(STR); + expect(require("partial-json").STR).toBe(STR); +}); +test("outermost_obj_partial with obj equals obj", () => { + const obj = parse('{"z": 0, "a": {"b": 2, "c": 3', OBJ); + expect(parse('{"z": 0, "a": {"b": 2, "c": 3', OUTERMOST_OBJ | OBJ)).toEqual( + obj + ); +}); + +test("nested_objects_with_outermost_obj", () => { + expect(parse('{"z": 0, "a": {"b": 2, "c": 3', OUTERMOST_OBJ)).toEqual({ + z: 0, + }); +}); + +test("outermost_arr_partial with arr equals arr", () => { + const arr = parse("[1, 2, [3, 4], [5,", ARR); + expect(parse("[1, 2, [3, 4], [5,", OUTERMOST_ARR | ARR)).toEqual(arr); +}); + +test("nested_arrays_with_outermost_arr", () => { + const result = parse("[1, 2, [3, 4], [5,", OUTERMOST_ARR); + expect(result).toEqual([1, 2, [3, 4]]); +}); + +test("simple_partial_array_with_object", () => { + expect(parse('[{"a":1},', ARR)).toEqual([{ a: 1 }]); +}); + +test("outermost_arr_with_nested_objects", () => { + expect(parse('[{"a": 1}, {"b": 2}, {"c":', OUTERMOST_ARR)).toEqual([ + { a: 1 }, + { b: 2 }, + ]); +}); + +test("outermost_arr_with_incomplete_nested_array", () => { + expect(parse("[1, 2, [3,", OUTERMOST_ARR)).toEqual([1, 2]); +}); + +test("outermost_arr_with_complete_nested_array", () => { + expect(parse("[1, 2, [3, 4], [5,", OUTERMOST_ARR)).toEqual([1, 2, [3, 4]]); +}); + +test("interleaved_partial_objects_and_arrays", () => { + const obj = + '[1,{"outer1": "yes"}, {"outer2":"yes", "inner2":{"included": "no"'; + expect(parse(obj, OUTERMOST_OBJ | OUTERMOST_ARR)).toEqual([ + 1, + { outer1: "yes" }, + { outer2: "yes" }, + ]); +}); + +test("complex_example", () => { + const obj = '{"init": 1, "out": ["a1", "b", [{ "c": 2, "d": 3, "e": 4 }]'; + expect(parse(obj, OUTERMOST_OBJ | OUTERMOST_ARR)).toEqual({ + init: 1, + out: ["a1", "b", [{ c: 2, d: 3, e: 4 }]], + }); });