@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.
@@ -2,103 +2,191 @@ import { JsonUtil } from "./JsonUtil";
2
2
 
3
3
  describe("JsonUtil", () => {
4
4
  describe("parse", () => {
5
- it("should handle string that starts with '{}'", () => {
6
- const jsonString = '{}{"name": "test", "value": 42}';
7
- const result = JsonUtil.parse(jsonString);
8
-
9
- expect(result).toEqual({ name: "test", value: 42 });
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
- it("should handle array with '{}' prefix", () => {
13
- const jsonString = '{}[1, 2, 3, "test"]';
14
- const result = JsonUtil.parse(jsonString);
15
-
16
- expect(result).toEqual([1, 2, 3, "test"]);
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
- it("should handle nested object with '{}' prefix", () => {
20
- const jsonString = '{}{"user": {"id": 1, "name": "John"}}';
21
- const result = JsonUtil.parse(jsonString);
22
-
23
- expect(result).toEqual({
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
- 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
- });
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
- it("should not modify string that doesn't start with '{}'", () => {
36
- const jsonString = '{"normal": "json"}';
37
- const result = JsonUtil.parse(jsonString);
38
-
39
- expect(result).toEqual({ normal: "json" });
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
- it("should handle missing closing brace in object", () => {
44
- const jsonString = '{"name": "test", "value": 42';
45
- const result = JsonUtil.parse(jsonString);
46
-
47
- expect(result).toEqual({ name: "test", value: 42 });
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
- it("should handle missing closing bracket in array", () => {
51
- const jsonString = '[1, 2, 3, "test"';
52
- const result = JsonUtil.parse(jsonString);
53
-
54
- expect(result).toEqual([1, 2, 3, "test"]);
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
- it("should handle nested object with missing closing brace", () => {
58
- const jsonString = '{"user": {"id": 1, "name": "John"}';
59
- const result = JsonUtil.parse(jsonString);
60
-
61
- expect(result).toEqual({
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
- it("should handle complex nested structure with missing closing brace", () => {
67
- const jsonString = '{"users": [{"id": 1, "name": "John"}, {"id": 2, "name": "Jane"}], "count": 2';
68
- const result = JsonUtil.parse(jsonString);
69
-
70
- expect(result).toEqual({
71
- users: [
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
- it("should handle both '{}' prefix and missing closing brace", () => {
80
- const jsonString = '{}{"name": "test", "value": 42';
81
- const result = JsonUtil.parse(jsonString);
82
-
83
- expect(result).toEqual({ name: "test", value: 42 });
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
- it("should handle both '{}' prefix and missing closing bracket in array", () => {
87
- const jsonString = '{}[1, 2, 3, "test"';
88
- const result = JsonUtil.parse(jsonString);
89
-
90
- expect(result).toEqual([1, 2, 3, "test"]);
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
- it("should throw error for completely invalid JSON", () => {
95
- const invalidJson = '{invalid: json without quotes}';
96
-
97
- expect(() => JsonUtil.parse(invalidJson)).toThrow();
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
- it("should throw error for empty string", () => {
101
- expect(() => JsonUtil.parse("")).toThrow();
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
  });
@@ -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(stripFirstBrace, correctMissingLastBrace)(str);
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
- };