@agentica/core 0.32.7 → 0.32.8
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/lib/index.mjs +4 -203
- package/lib/index.mjs.map +1 -1
- package/lib/utils/JsonUtil.d.ts +0 -1
- package/lib/utils/JsonUtil.js +3 -97
- package/lib/utils/JsonUtil.js.map +1 -1
- package/lib/utils/JsonUtil.spec.js +137 -69
- package/lib/utils/JsonUtil.spec.js.map +1 -1
- package/package.json +2 -1
- package/src/utils/JsonUtil.spec.ts +168 -80
- package/src/utils/JsonUtil.ts +4 -140
|
@@ -2,103 +2,191 @@ import { JsonUtil } from "./JsonUtil";
|
|
|
2
2
|
|
|
3
3
|
describe("JsonUtil", () => {
|
|
4
4
|
describe("parse", () => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
|
|
6
|
+
describe("Normal Operations", () => {
|
|
7
|
+
it("should parse standard valid JSON", () => {
|
|
8
|
+
const jsonString = '{"normal": "json"}';
|
|
9
|
+
const result = JsonUtil.parse(jsonString);
|
|
10
|
+
|
|
11
|
+
expect(result).toEqual({ normal: "json" });
|
|
12
|
+
});
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
it("should handle object with '{}' prefix", () => {
|
|
15
|
+
const jsonString = '{}{"name": "test", "value": 42}';
|
|
16
|
+
const result = JsonUtil.parse(jsonString);
|
|
17
|
+
|
|
18
|
+
expect(result).toEqual({ name: "test", value: 42 });
|
|
19
|
+
});
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
user: { id: 1, name: "John" }
|
|
21
|
+
it("should handle array with '{}' prefix", () => {
|
|
22
|
+
const jsonString = '{}[1, 2, 3, "test"]';
|
|
23
|
+
const result = JsonUtil.parse(jsonString);
|
|
24
|
+
|
|
25
|
+
expect(result).toEqual([1, 2, 3, "test"]);
|
|
25
26
|
});
|
|
26
|
-
});
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
it("should handle primitive values with '{}' prefix", () => {
|
|
29
|
+
expect(JsonUtil.parse('{}42')).toBe(42);
|
|
30
|
+
expect(JsonUtil.parse('{}"hello"')).toBe("hello");
|
|
31
|
+
expect(JsonUtil.parse('{}true')).toBe(true);
|
|
32
|
+
expect(JsonUtil.parse('{}null')).toBeNull();
|
|
33
|
+
});
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
it("should remove trailing comma in object", () => {
|
|
36
|
+
const jsonString = '{"name": "test", "value": 42,}';
|
|
37
|
+
const result = JsonUtil.parse(jsonString);
|
|
38
|
+
|
|
39
|
+
expect(result).toEqual({ name: "test", value: 42 });
|
|
40
|
+
});
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
42
|
+
it("should remove trailing comma in array", () => {
|
|
43
|
+
const jsonString = '[1, 2, 3, "test",]';
|
|
44
|
+
const result = JsonUtil.parse(jsonString);
|
|
45
|
+
|
|
46
|
+
expect(result).toEqual([1, 2, 3, "test"]);
|
|
47
|
+
});
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
it("should add missing closing brace in object", () => {
|
|
50
|
+
const jsonString = '{"name": "test", "value": 42';
|
|
51
|
+
const result = JsonUtil.parse(jsonString);
|
|
52
|
+
|
|
53
|
+
expect(result).toEqual({ name: "test", value: 42 });
|
|
54
|
+
});
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
user: { id: 1, name: "John" }
|
|
56
|
+
it("should add missing closing bracket in array", () => {
|
|
57
|
+
const jsonString = '[1, 2, 3, "test"';
|
|
58
|
+
const result = JsonUtil.parse(jsonString);
|
|
59
|
+
|
|
60
|
+
expect(result).toEqual([1, 2, 3, "test"]);
|
|
63
61
|
});
|
|
64
62
|
});
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{ id: 1, name: "John" },
|
|
73
|
-
{ id: 2, name: "Jane" }
|
|
74
|
-
],
|
|
75
|
-
count: 2
|
|
64
|
+
describe("Combined Features", () => {
|
|
65
|
+
it("should handle '{}' prefix and missing closing brace together", () => {
|
|
66
|
+
const jsonString = '{}{"name": "test", "value": 42';
|
|
67
|
+
const result = JsonUtil.parse(jsonString);
|
|
68
|
+
|
|
69
|
+
expect(result).toEqual({ name: "test", value: 42 });
|
|
76
70
|
});
|
|
77
|
-
});
|
|
78
71
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
72
|
+
it("should handle '{}' prefix and missing closing bracket together", () => {
|
|
73
|
+
const jsonString = '{}[1, 2, 3, "test"';
|
|
74
|
+
const result = JsonUtil.parse(jsonString);
|
|
75
|
+
|
|
76
|
+
expect(result).toEqual([1, 2, 3, "test"]);
|
|
77
|
+
});
|
|
85
78
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
79
|
+
it("should handle trailing comma in nested objects", () => {
|
|
80
|
+
const jsonString = '{"user": {"id": 1, "name": "John",}, "active": true,}';
|
|
81
|
+
const result = JsonUtil.parse(jsonString);
|
|
82
|
+
|
|
83
|
+
expect(result).toEqual({
|
|
84
|
+
user: { id: 1, name: "John" },
|
|
85
|
+
active: true
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should handle missing closing brace in nested objects", () => {
|
|
90
|
+
const jsonString = '{"user": {"id": 1, "name": "John"}';
|
|
91
|
+
const result = JsonUtil.parse(jsonString);
|
|
92
|
+
|
|
93
|
+
expect(result).toEqual({
|
|
94
|
+
user: { id: 1, name: "John" }
|
|
95
|
+
});
|
|
96
|
+
});
|
|
92
97
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
it("should handle missing closing brace in complex nested structure", () => {
|
|
99
|
+
const jsonString = '{"users": [{"id": 1, "name": "John"}, {"id": 2, "name": "Jane"}], "count": 2';
|
|
100
|
+
const result = JsonUtil.parse(jsonString);
|
|
101
|
+
|
|
102
|
+
expect(result).toEqual({
|
|
103
|
+
users: [
|
|
104
|
+
{ id: 1, name: "John" },
|
|
105
|
+
{ id: 2, name: "Jane" }
|
|
106
|
+
],
|
|
107
|
+
count: 2
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should apply all correction features together", () => {
|
|
112
|
+
const jsonString = '{}{"name": "test", "items": [1, 2, 3,], "user": {"id": 1, "name": "John",}';
|
|
113
|
+
const result = JsonUtil.parse(jsonString);
|
|
114
|
+
|
|
115
|
+
expect(result).toEqual({
|
|
116
|
+
name: "test",
|
|
117
|
+
items: [1, 2, 3],
|
|
118
|
+
user: { id: 1, name: "John" }
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should handle all issues simultaneously in complex nested structure", () => {
|
|
123
|
+
const jsonString = '{}{"data": {"users": [{"id": 1, "name": "John",}, {"id": 2, "name": "Jane",}], "meta": {"total": 2, "page": 1,}}, "status": "ok",';
|
|
124
|
+
const result = JsonUtil.parse(jsonString);
|
|
125
|
+
|
|
126
|
+
expect(result).toEqual({
|
|
127
|
+
data: {
|
|
128
|
+
users: [
|
|
129
|
+
{ id: 1, name: "John" },
|
|
130
|
+
{ id: 2, name: "Jane" }
|
|
131
|
+
],
|
|
132
|
+
meta: { total: 2, page: 1 }
|
|
133
|
+
},
|
|
134
|
+
status: "ok"
|
|
135
|
+
});
|
|
136
|
+
});
|
|
98
137
|
});
|
|
99
138
|
|
|
100
|
-
|
|
101
|
-
|
|
139
|
+
describe("Edge Cases", () => {
|
|
140
|
+
it("should handle empty object with '{}' prefix", () => {
|
|
141
|
+
const jsonString = '{}{}';
|
|
142
|
+
const result = JsonUtil.parse(jsonString);
|
|
143
|
+
|
|
144
|
+
expect(result).toEqual({});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should handle empty array with '{}' prefix", () => {
|
|
148
|
+
const jsonString = '{}[]';
|
|
149
|
+
const result = JsonUtil.parse(jsonString);
|
|
150
|
+
|
|
151
|
+
expect(result).toEqual([]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should handle nested object with '{}' prefix", () => {
|
|
155
|
+
const jsonString = '{}{"user": {"id": 1, "name": "John"}}';
|
|
156
|
+
const result = JsonUtil.parse(jsonString);
|
|
157
|
+
|
|
158
|
+
expect(result).toEqual({
|
|
159
|
+
user: { id: 1, name: "John" }
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should handle multiple trailing commas", () => {
|
|
164
|
+
const jsonString = '{"items": [1, 2, 3,,,], "count": 3,,,}';
|
|
165
|
+
const result = JsonUtil.parse(jsonString);
|
|
166
|
+
|
|
167
|
+
expect(result).toEqual({
|
|
168
|
+
items: [1, 2, 3],
|
|
169
|
+
count: 3
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("should handle JSON with whitespace and formatting issues", () => {
|
|
174
|
+
const jsonString = '{} { "name" : "test" , "value" : 42 , } ';
|
|
175
|
+
const result = JsonUtil.parse(jsonString);
|
|
176
|
+
|
|
177
|
+
expect(result).toEqual({ name: "test", value: 42 });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should throw error for completely invalid JSON", () => {
|
|
181
|
+
const invalidJson = '{invalid: json without quotes}';
|
|
182
|
+
|
|
183
|
+
expect(() => JsonUtil.parse(invalidJson)).toThrow();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("should throw error for empty string", () => {
|
|
187
|
+
expect(() => JsonUtil.parse("")).toThrow();
|
|
188
|
+
});
|
|
102
189
|
});
|
|
190
|
+
|
|
103
191
|
});
|
|
104
192
|
});
|
package/src/utils/JsonUtil.ts
CHANGED
|
@@ -1,150 +1,14 @@
|
|
|
1
|
+
import { addMissingBraces, removeEmptyObjectPrefix, removeTrailingCommas } from "es-jsonkit";
|
|
2
|
+
|
|
1
3
|
export const JsonUtil = {
|
|
2
4
|
parse,
|
|
3
5
|
};
|
|
4
6
|
|
|
5
7
|
function parse(str: string) {
|
|
6
|
-
const corrected = pipe(
|
|
8
|
+
const corrected = pipe(removeEmptyObjectPrefix, addMissingBraces, removeTrailingCommas)(str);
|
|
9
|
+
console.log(corrected);
|
|
7
10
|
return JSON.parse(corrected);
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
const pipe = (...fns: ((str: string) => string)[]) => (str: string) => fns.reduce((acc, fn) => fn(acc), str);
|
|
11
14
|
|
|
12
|
-
function stripFirstBrace(str: string) {
|
|
13
|
-
if(RegExp("^{}.").test(str) === true) {
|
|
14
|
-
return str.substring(2);
|
|
15
|
-
}
|
|
16
|
-
return str;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function correctMissingLastBrace(input: string): string {
|
|
20
|
-
const initial: ParseState = { s: "OUT", stack: [], line: 1, col: 0, edits: [] };
|
|
21
|
-
|
|
22
|
-
const scanned = Array.from(input).reduce<ParseState>((ps, ch, i) => {
|
|
23
|
-
const updated = ch === "\n"
|
|
24
|
-
? { ...ps, line: ps.line + 1, col: 0 }
|
|
25
|
-
: { ...ps, col: ps.col + 1 };
|
|
26
|
-
|
|
27
|
-
const tok = categorize(ch);
|
|
28
|
-
const trans = (table[updated.s] as Record<Token, Transition>)?.[tok];
|
|
29
|
-
return trans ? trans(updated, ch, i) : updated;
|
|
30
|
-
}, initial);
|
|
31
|
-
|
|
32
|
-
// Return original string if string is not closed (do not modify)
|
|
33
|
-
if (scanned.s !== "OUT") return input;
|
|
34
|
-
|
|
35
|
-
// Insert closing braces at the end for remaining open braces (LIFO)
|
|
36
|
-
const withTail = scanned.stack.length === 0
|
|
37
|
-
? scanned
|
|
38
|
-
: ((): ParseState => {
|
|
39
|
-
const closers = scanned.stack.slice().reverse().map(e => closeOf[e.type]).join("");
|
|
40
|
-
return {
|
|
41
|
-
...scanned,
|
|
42
|
-
edits: [...scanned.edits, { op: "insert", index: input.length, text: closers }],
|
|
43
|
-
stack: [],
|
|
44
|
-
};
|
|
45
|
-
})();
|
|
46
|
-
|
|
47
|
-
return applyEditsImmutable(input, withTail.edits);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Apply edits immutably
|
|
51
|
-
function applyEditsImmutable(src: string, edits: ReadonlyArray<Edit>): string {
|
|
52
|
-
const sorted = [...edits].sort((a, b) => a.index - b.index);
|
|
53
|
-
|
|
54
|
-
type Build = { out: string; cursor: number };
|
|
55
|
-
const built = sorted.reduce<Build>((acc, e) => {
|
|
56
|
-
const prefix = src.slice(acc.cursor, e.index);
|
|
57
|
-
const acc1 = { out: acc.out + prefix, cursor: e.index };
|
|
58
|
-
return e.op === "delete"
|
|
59
|
-
? { out: acc1.out, cursor: acc1.cursor + 1 }
|
|
60
|
-
: e.op === "replace"
|
|
61
|
-
? { out: acc1.out + e.text, cursor: acc1.cursor + 1 }
|
|
62
|
-
: /* insert */ { out: acc1.out + e.text, cursor: acc1.cursor };
|
|
63
|
-
}, { out: "", cursor: 0 });
|
|
64
|
-
|
|
65
|
-
return built.out + src.slice(built.cursor);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const openOf = Object.freeze<Readonly<Record<BraceClose, BraceOpen>>>({ "}": "{", "]": "[" });
|
|
69
|
-
const closeOf = Object.freeze<Readonly<Record<BraceOpen, BraceClose>>>({ "{": "}", "[": "]" });
|
|
70
|
-
|
|
71
|
-
const categorize = (ch: string): Token => {
|
|
72
|
-
switch (ch) {
|
|
73
|
-
case '"': return "DQUOTE";
|
|
74
|
-
case "\\": return "BSLASH";
|
|
75
|
-
case "{": return "OCB";
|
|
76
|
-
case "[": return "OSB";
|
|
77
|
-
case "}": return "CCB";
|
|
78
|
-
case "]": return "CSB";
|
|
79
|
-
case "\n": return "NEWLINE";
|
|
80
|
-
default: return "CHAR";
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
type Transition = (ps: ParseState, ch: string, i: number) => ParseState;
|
|
85
|
-
type Table = Readonly<Partial<Record<StateName, Partial<Record<Token, Transition>>>>>;
|
|
86
|
-
|
|
87
|
-
const push = (ps: ParseState, type: BraceOpen, index: number): ParseState =>
|
|
88
|
-
({ ...ps, stack: [...ps.stack, { type, index }] });
|
|
89
|
-
|
|
90
|
-
const withEdit = (ps: ParseState, edit: Edit): ParseState =>
|
|
91
|
-
({ ...ps, edits: [...ps.edits, edit] });
|
|
92
|
-
|
|
93
|
-
const popOrFix = (ps: ParseState, closer: BraceClose, idx: number): ParseState =>
|
|
94
|
-
((): ParseState => {
|
|
95
|
-
if (ps.stack.length === 0) {
|
|
96
|
-
// Extra closing brace → delete
|
|
97
|
-
return withEdit(ps, { op: "delete", index: idx });
|
|
98
|
-
}
|
|
99
|
-
const top = ps.stack[ps.stack.length - 1];
|
|
100
|
-
if (top !== undefined && top.type !== openOf[closer]) {
|
|
101
|
-
// Type mismatch → replace with expected value + pop
|
|
102
|
-
const expected = closeOf[top.type];
|
|
103
|
-
return withEdit({ ...ps, stack: ps.stack.slice(0, -1) }, { op: "replace", index: idx, text: expected });
|
|
104
|
-
}
|
|
105
|
-
// Normal matching → pop
|
|
106
|
-
return { ...ps, stack: ps.stack.slice(0, -1) };
|
|
107
|
-
})();
|
|
108
|
-
|
|
109
|
-
const table: Table = {
|
|
110
|
-
OUT: {
|
|
111
|
-
DQUOTE: (ps) => ({ ...ps, s: "IN" }),
|
|
112
|
-
OCB: (ps, _ch, i) => push(ps, "{", i),
|
|
113
|
-
OSB: (ps, _ch, i) => push(ps, "[", i),
|
|
114
|
-
CCB: (ps, _ch, i) => popOrFix(ps, "}", i),
|
|
115
|
-
CSB: (ps, _ch, i) => popOrFix(ps, "]", i),
|
|
116
|
-
},
|
|
117
|
-
IN: {
|
|
118
|
-
BSLASH: (ps) => ({ ...ps, s: "ESC" }),
|
|
119
|
-
DQUOTE: (ps) => ({ ...ps, s: "OUT" }),
|
|
120
|
-
},
|
|
121
|
-
ESC: {
|
|
122
|
-
DQUOTE: (ps) => ({ ...ps, s: "IN" }),
|
|
123
|
-
BSLASH: (ps) => ({ ...ps, s: "IN" }),
|
|
124
|
-
OCB: (ps) => ({ ...ps, s: "IN" }),
|
|
125
|
-
OSB: (ps) => ({ ...ps, s: "IN" }),
|
|
126
|
-
CCB: (ps) => ({ ...ps, s: "IN" }),
|
|
127
|
-
CSB: (ps) => ({ ...ps, s: "IN" }),
|
|
128
|
-
CHAR: (ps) => ({ ...ps, s: "IN" }),
|
|
129
|
-
NEWLINE:(ps) => ({ ...ps, s: "IN" }),
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
type StateName = "OUT" | "IN" | "ESC";
|
|
134
|
-
type BraceOpen = "{" | "[";
|
|
135
|
-
type BraceClose = "}" | "]";
|
|
136
|
-
type Token = "DQUOTE" | "BSLASH" | "OCB" | "OSB" | "CCB" | "CSB" | "NEWLINE" | "CHAR";
|
|
137
|
-
|
|
138
|
-
type StackEntry = { type: BraceOpen; index: number };
|
|
139
|
-
type Edit =
|
|
140
|
-
| { op: "delete"; index: number }
|
|
141
|
-
| { op: "replace"; index: number; text: string }
|
|
142
|
-
| { op: "insert"; index: number; text: string };
|
|
143
|
-
|
|
144
|
-
type ParseState = {
|
|
145
|
-
s: StateName;
|
|
146
|
-
stack: ReadonlyArray<StackEntry>;
|
|
147
|
-
line: number;
|
|
148
|
-
col: number;
|
|
149
|
-
edits: ReadonlyArray<Edit>;
|
|
150
|
-
};
|