@a2ui-sdk/utils 0.1.1 → 0.2.0

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.
@@ -1,5 +0,0 @@
1
- /**
2
- * Tests for the public interpolation API.
3
- * Tests the new refactored parser module.
4
- */
5
- export {};
@@ -1,154 +0,0 @@
1
- /**
2
- * Tests for the public interpolation API.
3
- * Tests the new refactored parser module.
4
- */
5
- import { describe, it, expect } from 'vitest';
6
- import { parseInterpolation, interpolate } from './interpolation/index.js';
7
- describe('parseInterpolation', () => {
8
- it('should parse simple path expression', () => {
9
- const ast = parseInterpolation('${/user/name}');
10
- expect(ast.type).toBe('interpolatedString');
11
- expect(ast.parts).toHaveLength(1);
12
- const pathNode = ast.parts[0];
13
- expect(pathNode.type).toBe('path');
14
- expect(pathNode.path).toBe('/user/name');
15
- expect(pathNode.absolute).toBe(true);
16
- });
17
- it('should parse mixed content', () => {
18
- const ast = parseInterpolation('Hello, ${/user/name}!');
19
- expect(ast.parts).toHaveLength(3);
20
- expect(ast.parts[0].type).toBe('literal');
21
- expect(ast.parts[0].value).toBe('Hello, ');
22
- expect(ast.parts[1].type).toBe('path');
23
- expect(ast.parts[1].path).toBe('/user/name');
24
- expect(ast.parts[2].type).toBe('literal');
25
- expect(ast.parts[2].value).toBe('!');
26
- });
27
- it('should parse function call', () => {
28
- const ast = parseInterpolation('${now()}');
29
- const funcNode = ast.parts[0];
30
- expect(funcNode.type).toBe('functionCall');
31
- expect(funcNode.name).toBe('now');
32
- expect(funcNode.args).toHaveLength(0);
33
- });
34
- it('should parse nested expressions', () => {
35
- const ast = parseInterpolation('${upper(${/name})}');
36
- const funcNode = ast.parts[0];
37
- expect(funcNode.type).toBe('functionCall');
38
- expect(funcNode.name).toBe('upper');
39
- expect(funcNode.args).toHaveLength(1);
40
- const arg = funcNode.args[0];
41
- expect(arg.type).toBe('path');
42
- expect(arg.path).toBe('/name');
43
- });
44
- it('should handle escaped expressions', () => {
45
- const ast = parseInterpolation('\\${escaped}');
46
- expect(ast.parts).toHaveLength(1);
47
- expect(ast.parts[0].value).toBe('${escaped}');
48
- });
49
- it('should parse relative paths', () => {
50
- const ast = parseInterpolation('${name}');
51
- const pathNode = ast.parts[0];
52
- expect(pathNode.type).toBe('path');
53
- expect(pathNode.path).toBe('name');
54
- expect(pathNode.absolute).toBe(false);
55
- });
56
- });
57
- describe('interpolate', () => {
58
- const dataModel = {
59
- user: {
60
- name: 'John',
61
- age: 30,
62
- },
63
- stats: {
64
- count: 42,
65
- active: true,
66
- },
67
- items: ['a', 'b', 'c'],
68
- };
69
- it('should interpolate single value', () => {
70
- expect(interpolate('Hello, ${/user/name}!', dataModel)).toBe('Hello, John!');
71
- });
72
- it('should interpolate multiple values', () => {
73
- expect(interpolate('${/user/name} is ${/user/age} years old', dataModel)).toBe('John is 30 years old');
74
- });
75
- it('should handle number values', () => {
76
- expect(interpolate('Count: ${/stats/count}', dataModel)).toBe('Count: 42');
77
- });
78
- it('should handle boolean values', () => {
79
- expect(interpolate('Active: ${/stats/active}', dataModel)).toBe('Active: true');
80
- });
81
- it('should handle array values as JSON', () => {
82
- expect(interpolate('Items: ${/items}', dataModel)).toBe('Items: ["a","b","c"]');
83
- });
84
- it('should handle object values as JSON', () => {
85
- expect(interpolate('User: ${/user}', dataModel)).toBe('User: {"name":"John","age":30}');
86
- });
87
- it('should handle undefined values as empty string', () => {
88
- expect(interpolate('Missing: ${/nonexistent}', dataModel)).toBe('Missing: ');
89
- expect(interpolate('${/a}${/b}${/c}', dataModel)).toBe('');
90
- });
91
- it('should handle null values as empty string', () => {
92
- const modelWithNull = { value: null };
93
- expect(interpolate('Value: ${/value}', modelWithNull)).toBe('Value: ');
94
- });
95
- it('should preserve text without interpolation', () => {
96
- expect(interpolate('Hello, World!', dataModel)).toBe('Hello, World!');
97
- expect(interpolate('No variables here', dataModel)).toBe('No variables here');
98
- });
99
- it('should unescape escaped expressions', () => {
100
- expect(interpolate('Escaped \\${/user/name}', dataModel)).toBe('Escaped ${/user/name}');
101
- expect(interpolate('\\${a} and \\${b}', dataModel)).toBe('${a} and ${b}');
102
- });
103
- it('should handle mix of escaped and unescaped', () => {
104
- expect(interpolate('\\${escaped} ${/user/name}', dataModel)).toBe('${escaped} John');
105
- });
106
- describe('with basePath', () => {
107
- it('should resolve relative paths with basePath', () => {
108
- expect(interpolate('Name: ${name}', dataModel, '/user')).toBe('Name: John');
109
- expect(interpolate('Age: ${age}', dataModel, '/user')).toBe('Age: 30');
110
- });
111
- it('should handle absolute paths even with basePath', () => {
112
- expect(interpolate('Count: ${/stats/count}', dataModel, '/user')).toBe('Count: 42');
113
- });
114
- it('should handle mix of relative and absolute', () => {
115
- expect(interpolate('${name} has ${/stats/count} items', dataModel, '/user')).toBe('John has 42 items');
116
- });
117
- });
118
- describe('function calls', () => {
119
- const testFunctions = {
120
- upper: (str) => String(str).toUpperCase(),
121
- lower: (str) => String(str).toLowerCase(),
122
- add: (...args) => args.reduce((sum, val) => sum + Number(val), 0),
123
- };
124
- it('should invoke functions from context', () => {
125
- expect(interpolate("${upper('hello')}", {}, null, testFunctions)).toBe('HELLO');
126
- expect(interpolate("${lower('HELLO')}", {}, null, testFunctions)).toBe('hello');
127
- expect(interpolate('${add(1, 2, 3)}', {}, null, testFunctions)).toBe('6');
128
- });
129
- it('should handle function with path arguments', () => {
130
- expect(interpolate('${upper(${/user/name})}', dataModel, null, testFunctions)).toBe('JOHN');
131
- });
132
- it('should handle nested function calls', () => {
133
- expect(interpolate('${add(${/user/age}, 10)}', dataModel, null, testFunctions)).toBe('40');
134
- });
135
- });
136
- describe('JSON Pointer escapes', () => {
137
- it('should resolve keys with forward slash', () => {
138
- const model = { 'a/b': 'value' };
139
- expect(interpolate('${/a~1b}', model)).toBe('value');
140
- });
141
- it('should resolve keys with tilde', () => {
142
- const model = { 'm~n': 'value' };
143
- expect(interpolate('${/m~0n}', model)).toBe('value');
144
- });
145
- });
146
- describe('custom functions', () => {
147
- it('should use custom functions', () => {
148
- const customFunctions = {
149
- greet: (name) => `Hello, ${name}!`,
150
- };
151
- expect(interpolate("${greet('World')}", {}, null, customFunctions)).toBe('Hello, World!');
152
- });
153
- });
154
- });
@@ -1,6 +0,0 @@
1
- /**
2
- * pathUtils Tests
3
- *
4
- * Tests for path utility functions used in A2UI 0.9 data model operations.
5
- */
6
- export {};
@@ -1,310 +0,0 @@
1
- /**
2
- * pathUtils Tests
3
- *
4
- * Tests for path utility functions used in A2UI 0.9 data model operations.
5
- */
6
- import { describe, it, expect } from 'vitest';
7
- import { parseJsonPointer, getValueByPath, setValueByPath, normalizePath, isAbsolutePath, resolvePath, joinPaths, } from './pathUtils.js';
8
- describe('pathUtils', () => {
9
- describe('parseJsonPointer', () => {
10
- it('should return empty array for empty path', () => {
11
- expect(parseJsonPointer('')).toEqual([]);
12
- });
13
- it('should return empty array for root path', () => {
14
- expect(parseJsonPointer('/')).toEqual([]);
15
- });
16
- it('should parse simple path', () => {
17
- expect(parseJsonPointer('/user')).toEqual(['user']);
18
- });
19
- it('should parse nested path', () => {
20
- expect(parseJsonPointer('/user/name')).toEqual(['user', 'name']);
21
- });
22
- it('should parse path with array index', () => {
23
- expect(parseJsonPointer('/items/0')).toEqual(['items', '0']);
24
- });
25
- it('should parse deeply nested path', () => {
26
- expect(parseJsonPointer('/a/b/c/d')).toEqual(['a', 'b', 'c', 'd']);
27
- });
28
- it('should unescape ~1 to /', () => {
29
- expect(parseJsonPointer('/a~1b')).toEqual(['a/b']);
30
- });
31
- it('should unescape ~0 to ~', () => {
32
- expect(parseJsonPointer('/m~0n')).toEqual(['m~n']);
33
- });
34
- it('should handle combined escapes', () => {
35
- expect(parseJsonPointer('/~0~1')).toEqual(['~/']);
36
- });
37
- it('should handle path without leading slash', () => {
38
- expect(parseJsonPointer('user/name')).toEqual(['user', 'name']);
39
- });
40
- });
41
- describe('getValueByPath', () => {
42
- const testModel = {
43
- user: {
44
- name: 'John',
45
- age: 30,
46
- profile: {
47
- email: 'john@example.com',
48
- active: true,
49
- },
50
- },
51
- items: ['a', 'b', 'c'],
52
- count: 42,
53
- nested: {
54
- array: [{ id: 1 }, { id: 2 }],
55
- },
56
- };
57
- it('should return entire data model for empty path', () => {
58
- expect(getValueByPath(testModel, '')).toEqual(testModel);
59
- });
60
- it('should return entire data model for root path', () => {
61
- expect(getValueByPath(testModel, '/')).toEqual(testModel);
62
- });
63
- it('should get top-level string value', () => {
64
- const model = { name: 'test' };
65
- expect(getValueByPath(model, '/name')).toBe('test');
66
- });
67
- it('should get top-level number value', () => {
68
- expect(getValueByPath(testModel, '/count')).toBe(42);
69
- });
70
- it('should get top-level array value', () => {
71
- expect(getValueByPath(testModel, '/items')).toEqual(['a', 'b', 'c']);
72
- });
73
- it('should get nested object value', () => {
74
- expect(getValueByPath(testModel, '/user')).toEqual({
75
- name: 'John',
76
- age: 30,
77
- profile: {
78
- email: 'john@example.com',
79
- active: true,
80
- },
81
- });
82
- });
83
- it('should get deeply nested string value', () => {
84
- expect(getValueByPath(testModel, '/user/name')).toBe('John');
85
- });
86
- it('should get deeply nested number value', () => {
87
- expect(getValueByPath(testModel, '/user/age')).toBe(30);
88
- });
89
- it('should get deeply nested object value', () => {
90
- expect(getValueByPath(testModel, '/user/profile')).toEqual({
91
- email: 'john@example.com',
92
- active: true,
93
- });
94
- });
95
- it('should get very deeply nested value', () => {
96
- expect(getValueByPath(testModel, '/user/profile/email')).toBe('john@example.com');
97
- expect(getValueByPath(testModel, '/user/profile/active')).toBe(true);
98
- });
99
- it('should get array element by index', () => {
100
- expect(getValueByPath(testModel, '/items/0')).toBe('a');
101
- expect(getValueByPath(testModel, '/items/1')).toBe('b');
102
- expect(getValueByPath(testModel, '/items/2')).toBe('c');
103
- });
104
- it('should get nested array element property', () => {
105
- expect(getValueByPath(testModel, '/nested/array/0/id')).toBe(1);
106
- expect(getValueByPath(testModel, '/nested/array/1/id')).toBe(2);
107
- });
108
- it('should return undefined for non-existent path', () => {
109
- expect(getValueByPath(testModel, '/nonexistent')).toBeUndefined();
110
- });
111
- it('should return undefined for non-existent nested path', () => {
112
- expect(getValueByPath(testModel, '/user/nonexistent')).toBeUndefined();
113
- });
114
- it('should return undefined for path through non-object', () => {
115
- expect(getValueByPath(testModel, '/count/nested')).toBeUndefined();
116
- });
117
- it('should return undefined for out-of-bounds array index', () => {
118
- expect(getValueByPath(testModel, '/items/10')).toBeUndefined();
119
- });
120
- it('should return undefined for non-numeric array index', () => {
121
- expect(getValueByPath(testModel, '/items/foo')).toBeUndefined();
122
- });
123
- it('should return undefined when intermediate value is null', () => {
124
- const model = { user: null };
125
- expect(getValueByPath(model, '/user/name')).toBeUndefined();
126
- });
127
- it('should return undefined when intermediate value is undefined', () => {
128
- const model = { user: undefined };
129
- expect(getValueByPath(model, '/user/name')).toBeUndefined();
130
- });
131
- it('should handle empty data model', () => {
132
- expect(getValueByPath({}, '/user/name')).toBeUndefined();
133
- });
134
- });
135
- describe('setValueByPath', () => {
136
- it('should replace entire model for root path with object value', () => {
137
- const model = { a: 1 };
138
- const result = setValueByPath(model, '/', { b: 2 });
139
- expect(result).toEqual({ b: 2 });
140
- });
141
- it('should return empty model for root path with undefined value', () => {
142
- const model = { a: 1 };
143
- const result = setValueByPath(model, '/', undefined);
144
- expect(result).toEqual({});
145
- });
146
- it('should return original model for root path with non-object value', () => {
147
- const model = { a: 1 };
148
- const result = setValueByPath(model, '/', 'string');
149
- expect(result).toEqual({ a: 1 });
150
- });
151
- it('should set top-level value', () => {
152
- const model = { a: 1 };
153
- const result = setValueByPath(model, '/b', 2);
154
- expect(result).toEqual({ a: 1, b: 2 });
155
- });
156
- it('should update existing top-level value', () => {
157
- const model = { a: 1 };
158
- const result = setValueByPath(model, '/a', 2);
159
- expect(result).toEqual({ a: 2 });
160
- });
161
- it('should delete value with undefined', () => {
162
- const model = { a: 1, b: 2 };
163
- const result = setValueByPath(model, '/a', undefined);
164
- expect(result).toEqual({ b: 2 });
165
- });
166
- it('should set nested value in existing object', () => {
167
- const model = { user: { name: 'John' } };
168
- const result = setValueByPath(model, '/user/age', 30);
169
- expect(result).toEqual({ user: { name: 'John', age: 30 } });
170
- });
171
- it('should update existing nested value', () => {
172
- const model = { user: { name: 'John' } };
173
- const result = setValueByPath(model, '/user/name', 'Jane');
174
- expect(result).toEqual({ user: { name: 'Jane' } });
175
- });
176
- it('should create nested structure if not exists', () => {
177
- const model = {};
178
- const result = setValueByPath(model, '/user/profile/email', 'test@test.com');
179
- expect(result).toEqual({
180
- user: { profile: { email: 'test@test.com' } },
181
- });
182
- });
183
- it('should set array element', () => {
184
- const model = { items: ['a', 'b', 'c'] };
185
- const result = setValueByPath(model, '/items/1', 'x');
186
- expect(result).toEqual({ items: ['a', 'x', 'c'] });
187
- });
188
- it('should delete array element', () => {
189
- const model = { items: ['a', 'b', 'c'] };
190
- const result = setValueByPath(model, '/items/1', undefined);
191
- expect(result).toEqual({ items: ['a', 'c'] });
192
- });
193
- it('should be immutable - not modify original model', () => {
194
- const model = { user: { name: 'John' } };
195
- const result = setValueByPath(model, '/user/name', 'Jane');
196
- expect(model).toEqual({ user: { name: 'John' } });
197
- expect(result).toEqual({ user: { name: 'Jane' } });
198
- });
199
- it('should handle setting null value', () => {
200
- const model = { user: { name: 'John' } };
201
- const result = setValueByPath(model, '/user/name', null);
202
- expect(result).toEqual({ user: { name: null } });
203
- });
204
- it('should handle setting array value', () => {
205
- const model = { items: [] };
206
- const result = setValueByPath(model, '/items', ['a', 'b']);
207
- expect(result).toEqual({ items: ['a', 'b'] });
208
- });
209
- it('should handle setting object value', () => {
210
- const model = { user: null };
211
- const result = setValueByPath(model, '/user', { name: 'John' });
212
- expect(result).toEqual({ user: { name: 'John' } });
213
- });
214
- });
215
- describe('normalizePath', () => {
216
- it('should add leading slash if missing', () => {
217
- expect(normalizePath('user/name')).toBe('/user/name');
218
- });
219
- it('should keep existing leading slash', () => {
220
- expect(normalizePath('/user/name')).toBe('/user/name');
221
- });
222
- it('should remove trailing slash', () => {
223
- expect(normalizePath('/user/name/')).toBe('/user/name');
224
- });
225
- it('should keep single root slash', () => {
226
- expect(normalizePath('/')).toBe('/');
227
- });
228
- it('should add leading and remove trailing slash', () => {
229
- expect(normalizePath('user/name/')).toBe('/user/name');
230
- });
231
- it('should trim whitespace', () => {
232
- expect(normalizePath(' /user/name ')).toBe('/user/name');
233
- });
234
- it('should handle empty string', () => {
235
- expect(normalizePath('')).toBe('/');
236
- });
237
- it('should handle whitespace only', () => {
238
- expect(normalizePath(' ')).toBe('/');
239
- });
240
- });
241
- describe('isAbsolutePath', () => {
242
- it('should return true for path starting with "/"', () => {
243
- expect(isAbsolutePath('/user/name')).toBe(true);
244
- });
245
- it('should return true for root path', () => {
246
- expect(isAbsolutePath('/')).toBe(true);
247
- });
248
- it('should return false for relative path', () => {
249
- expect(isAbsolutePath('user/name')).toBe(false);
250
- });
251
- it('should return false for simple name', () => {
252
- expect(isAbsolutePath('name')).toBe(false);
253
- });
254
- it('should return false for empty string', () => {
255
- expect(isAbsolutePath('')).toBe(false);
256
- });
257
- });
258
- describe('resolvePath', () => {
259
- it('should return absolute path as-is', () => {
260
- expect(resolvePath('/user/name', '/items/0')).toBe('/user/name');
261
- });
262
- it('should resolve relative path against base path', () => {
263
- expect(resolvePath('name', '/items/0')).toBe('/items/0/name');
264
- });
265
- it('should resolve relative path against root scope', () => {
266
- expect(resolvePath('name', null)).toBe('/name');
267
- });
268
- it('should resolve relative path against "/" base', () => {
269
- expect(resolvePath('name', '/')).toBe('/name');
270
- });
271
- it('should handle complex relative path', () => {
272
- expect(resolvePath('profile/email', '/users/0')).toBe('/users/0/profile/email');
273
- });
274
- it('should normalize absolute path', () => {
275
- expect(resolvePath('/user/name/', '/items/0')).toBe('/user/name');
276
- });
277
- });
278
- describe('joinPaths', () => {
279
- it('should join base and relative paths', () => {
280
- expect(joinPaths('/user', 'name')).toBe('/user/name');
281
- });
282
- it('should handle leading slash in relative path', () => {
283
- expect(joinPaths('/user', '/name')).toBe('/user/name');
284
- });
285
- it('should handle trailing slash in base path', () => {
286
- expect(joinPaths('/user/', 'name')).toBe('/user/name');
287
- });
288
- it('should handle both leading and trailing slashes', () => {
289
- expect(joinPaths('/user/', '/name/')).toBe('/user/name');
290
- });
291
- it('should return base path for empty relative path', () => {
292
- expect(joinPaths('/user', '')).toBe('/user');
293
- });
294
- it('should handle root base path', () => {
295
- expect(joinPaths('/', 'user')).toBe('/user');
296
- });
297
- it('should handle root base path with leading slash in relative', () => {
298
- expect(joinPaths('/', '/user')).toBe('/user');
299
- });
300
- it('should normalize base path without leading slash', () => {
301
- expect(joinPaths('user', 'name')).toBe('/user/name');
302
- });
303
- it('should handle multi-segment relative path', () => {
304
- expect(joinPaths('/base', 'a/b/c')).toBe('/base/a/b/c');
305
- });
306
- it('should handle whitespace in relative path', () => {
307
- expect(joinPaths('/user', ' name ')).toBe('/user/name');
308
- });
309
- });
310
- });
@@ -1,4 +0,0 @@
1
- /**
2
- * Tests for validation utilities.
3
- */
4
- export {};