@a2ui-sdk/utils 0.0.3 → 0.1.1
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/README.md +154 -0
- package/dist/0.8/dataBinding.d.ts +55 -0
- package/dist/0.8/dataBinding.js +118 -0
- package/dist/0.8/dataBinding.test.d.ts +6 -0
- package/dist/0.8/dataBinding.test.js +271 -0
- package/dist/0.8/index.d.ts +2 -0
- package/dist/0.8/index.js +2 -0
- package/dist/0.8/pathUtils.d.ts +42 -0
- package/dist/0.8/pathUtils.js +111 -0
- package/dist/0.8/pathUtils.test.d.ts +6 -0
- package/dist/0.8/pathUtils.test.js +211 -0
- package/dist/0.9/dataBinding.d.ts +103 -0
- package/dist/0.9/dataBinding.js +165 -0
- package/dist/0.9/dataBinding.test.d.ts +6 -0
- package/dist/0.9/dataBinding.test.js +180 -0
- package/dist/0.9/index.d.ts +4 -1
- package/dist/0.9/index.js +4 -1
- package/dist/0.9/interpolation/index.d.ts +0 -4
- package/dist/0.9/interpolation/index.js +0 -3
- package/dist/0.9/interpolation.test.js +1 -1
- package/dist/0.9/pathUtils.d.ts +0 -12
- package/dist/0.9/pathUtils.js +0 -19
- package/dist/0.9/pathUtils.test.js +1 -21
- package/dist/0.9/validation.d.ts +50 -0
- package/dist/0.9/validation.js +162 -0
- package/dist/0.9/validation.test.d.ts +4 -0
- package/dist/0.9/validation.test.js +307 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +11 -4
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path utility functions for A2UI data model operations.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Gets a value from the data model by path.
|
|
6
|
+
*
|
|
7
|
+
* @param dataModel - The data model to read from
|
|
8
|
+
* @param path - The path to the value (e.g., "/user/name")
|
|
9
|
+
* @returns The value at the path, or undefined if not found
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const model = { user: { name: "John" } };
|
|
13
|
+
* getValueByPath(model, "/user/name"); // "John"
|
|
14
|
+
* getValueByPath(model, "/user/age"); // undefined
|
|
15
|
+
*/
|
|
16
|
+
export function getValueByPath(dataModel, path) {
|
|
17
|
+
if (!path || path === '/') {
|
|
18
|
+
return dataModel;
|
|
19
|
+
}
|
|
20
|
+
// Split path: "/user/name" -> ["user", "name"]
|
|
21
|
+
const keys = path.split('/').filter(Boolean);
|
|
22
|
+
let current = dataModel;
|
|
23
|
+
for (const key of keys) {
|
|
24
|
+
if (current === null || current === undefined) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
if (typeof current !== 'object') {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
current = current[key];
|
|
31
|
+
}
|
|
32
|
+
return current;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Sets a value in the data model by path, returning a new data model.
|
|
36
|
+
* This function is immutable - it does not modify the original data model.
|
|
37
|
+
*
|
|
38
|
+
* @param dataModel - The data model to update
|
|
39
|
+
* @param path - The path to set (e.g., "/user/name")
|
|
40
|
+
* @param value - The value to set
|
|
41
|
+
* @returns A new data model with the value set
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* const model = { user: { name: "John" } };
|
|
45
|
+
* setValueByPath(model, "/user/name", "Jane");
|
|
46
|
+
* // Returns: { user: { name: "Jane" } }
|
|
47
|
+
*/
|
|
48
|
+
export function setValueByPath(dataModel, path, value) {
|
|
49
|
+
if (!path || path === '/') {
|
|
50
|
+
// Replace the entire data model
|
|
51
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
52
|
+
return { ...dataModel, ...value };
|
|
53
|
+
}
|
|
54
|
+
return dataModel;
|
|
55
|
+
}
|
|
56
|
+
// Split path: "/user/name" -> ["user", "name"]
|
|
57
|
+
const keys = path.split('/').filter(Boolean);
|
|
58
|
+
// Recursive helper to build the new object structure
|
|
59
|
+
function setNested(obj, remainingKeys, val) {
|
|
60
|
+
if (remainingKeys.length === 0) {
|
|
61
|
+
// This shouldn't happen, but handle gracefully
|
|
62
|
+
if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
|
|
63
|
+
return { ...obj, ...val };
|
|
64
|
+
}
|
|
65
|
+
return obj;
|
|
66
|
+
}
|
|
67
|
+
const [key, ...rest] = remainingKeys;
|
|
68
|
+
if (rest.length === 0) {
|
|
69
|
+
// Last key - set the value
|
|
70
|
+
return {
|
|
71
|
+
...obj,
|
|
72
|
+
[key]: val,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// Intermediate key - recurse
|
|
76
|
+
const existingValue = obj[key];
|
|
77
|
+
const nestedObj = typeof existingValue === 'object' &&
|
|
78
|
+
existingValue !== null &&
|
|
79
|
+
!Array.isArray(existingValue)
|
|
80
|
+
? existingValue
|
|
81
|
+
: {};
|
|
82
|
+
return {
|
|
83
|
+
...obj,
|
|
84
|
+
[key]: setNested(nestedObj, rest, val),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return setNested(dataModel, keys, value);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Merges data into the data model at a given path.
|
|
91
|
+
* This is used for dataModelUpdate messages where contents are merged.
|
|
92
|
+
*
|
|
93
|
+
* @param dataModel - The data model to update
|
|
94
|
+
* @param path - The path to merge at (e.g., "/form")
|
|
95
|
+
* @param data - The data to merge
|
|
96
|
+
* @returns A new data model with the data merged
|
|
97
|
+
*/
|
|
98
|
+
export function mergeAtPath(dataModel, path, data) {
|
|
99
|
+
if (!path || path === '/') {
|
|
100
|
+
// Merge at root
|
|
101
|
+
return { ...dataModel, ...data };
|
|
102
|
+
}
|
|
103
|
+
// Get current value at path
|
|
104
|
+
const current = getValueByPath(dataModel, path);
|
|
105
|
+
const currentObj = typeof current === 'object' && current !== null && !Array.isArray(current)
|
|
106
|
+
? current
|
|
107
|
+
: {};
|
|
108
|
+
// Merge and set
|
|
109
|
+
const merged = { ...currentObj, ...data };
|
|
110
|
+
return setValueByPath(dataModel, path, merged);
|
|
111
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pathUtils Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for path utility functions used in A2UI data model operations.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import { getValueByPath, setValueByPath, mergeAtPath } from './pathUtils.js';
|
|
8
|
+
describe('pathUtils', () => {
|
|
9
|
+
describe('getValueByPath', () => {
|
|
10
|
+
const testModel = {
|
|
11
|
+
user: {
|
|
12
|
+
name: 'John',
|
|
13
|
+
age: 30,
|
|
14
|
+
profile: {
|
|
15
|
+
email: 'john@example.com',
|
|
16
|
+
active: true,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
items: ['a', 'b', 'c'],
|
|
20
|
+
count: 42,
|
|
21
|
+
};
|
|
22
|
+
it('should return entire data model for empty path', () => {
|
|
23
|
+
expect(getValueByPath(testModel, '')).toEqual(testModel);
|
|
24
|
+
});
|
|
25
|
+
it('should return entire data model for root path', () => {
|
|
26
|
+
expect(getValueByPath(testModel, '/')).toEqual(testModel);
|
|
27
|
+
});
|
|
28
|
+
it('should get top-level string value', () => {
|
|
29
|
+
const model = { name: 'test' };
|
|
30
|
+
expect(getValueByPath(model, '/name')).toBe('test');
|
|
31
|
+
});
|
|
32
|
+
it('should get top-level number value', () => {
|
|
33
|
+
expect(getValueByPath(testModel, '/count')).toBe(42);
|
|
34
|
+
});
|
|
35
|
+
it('should get top-level array value', () => {
|
|
36
|
+
expect(getValueByPath(testModel, '/items')).toEqual(['a', 'b', 'c']);
|
|
37
|
+
});
|
|
38
|
+
it('should get nested object value', () => {
|
|
39
|
+
expect(getValueByPath(testModel, '/user')).toEqual({
|
|
40
|
+
name: 'John',
|
|
41
|
+
age: 30,
|
|
42
|
+
profile: {
|
|
43
|
+
email: 'john@example.com',
|
|
44
|
+
active: true,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
it('should get deeply nested string value', () => {
|
|
49
|
+
expect(getValueByPath(testModel, '/user/name')).toBe('John');
|
|
50
|
+
});
|
|
51
|
+
it('should get deeply nested number value', () => {
|
|
52
|
+
expect(getValueByPath(testModel, '/user/age')).toBe(30);
|
|
53
|
+
});
|
|
54
|
+
it('should get deeply nested object value', () => {
|
|
55
|
+
expect(getValueByPath(testModel, '/user/profile')).toEqual({
|
|
56
|
+
email: 'john@example.com',
|
|
57
|
+
active: true,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
it('should get very deeply nested value', () => {
|
|
61
|
+
expect(getValueByPath(testModel, '/user/profile/email')).toBe('john@example.com');
|
|
62
|
+
expect(getValueByPath(testModel, '/user/profile/active')).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
it('should return undefined for non-existent path', () => {
|
|
65
|
+
expect(getValueByPath(testModel, '/nonexistent')).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
it('should return undefined for non-existent nested path', () => {
|
|
68
|
+
expect(getValueByPath(testModel, '/user/nonexistent')).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
it('should return undefined for path through non-object', () => {
|
|
71
|
+
expect(getValueByPath(testModel, '/count/nested')).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
it('should return undefined when intermediate value is null', () => {
|
|
74
|
+
const model = { user: null };
|
|
75
|
+
expect(getValueByPath(model, '/user/name')).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
it('should return undefined when intermediate value is undefined', () => {
|
|
78
|
+
const model = { user: undefined };
|
|
79
|
+
expect(getValueByPath(model, '/user/name')).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
it('should handle paths without leading slash', () => {
|
|
82
|
+
expect(getValueByPath(testModel, 'user/name')).toBe('John');
|
|
83
|
+
});
|
|
84
|
+
it('should handle empty data model', () => {
|
|
85
|
+
expect(getValueByPath({}, '/user/name')).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe('setValueByPath', () => {
|
|
89
|
+
it('should return merged object for empty path with object value', () => {
|
|
90
|
+
const model = { a: 1 };
|
|
91
|
+
const result = setValueByPath(model, '', { b: 2 });
|
|
92
|
+
expect(result).toEqual({ a: 1, b: 2 });
|
|
93
|
+
});
|
|
94
|
+
it('should return merged object for root path with object value', () => {
|
|
95
|
+
const model = { a: 1 };
|
|
96
|
+
const result = setValueByPath(model, '/', { b: 2 });
|
|
97
|
+
expect(result).toEqual({ a: 1, b: 2 });
|
|
98
|
+
});
|
|
99
|
+
it('should return original model for root path with non-object value', () => {
|
|
100
|
+
const model = { a: 1 };
|
|
101
|
+
const result = setValueByPath(model, '/', 'string');
|
|
102
|
+
expect(result).toEqual({ a: 1 });
|
|
103
|
+
});
|
|
104
|
+
it('should return original model for root path with array value', () => {
|
|
105
|
+
const model = { a: 1 };
|
|
106
|
+
const result = setValueByPath(model, '/', [1, 2, 3]);
|
|
107
|
+
expect(result).toEqual({ a: 1 });
|
|
108
|
+
});
|
|
109
|
+
it('should set top-level value', () => {
|
|
110
|
+
const model = { a: 1 };
|
|
111
|
+
const result = setValueByPath(model, '/b', 2);
|
|
112
|
+
expect(result).toEqual({ a: 1, b: 2 });
|
|
113
|
+
});
|
|
114
|
+
it('should update existing top-level value', () => {
|
|
115
|
+
const model = { a: 1 };
|
|
116
|
+
const result = setValueByPath(model, '/a', 2);
|
|
117
|
+
expect(result).toEqual({ a: 2 });
|
|
118
|
+
});
|
|
119
|
+
it('should set nested value in existing object', () => {
|
|
120
|
+
const model = { user: { name: 'John' } };
|
|
121
|
+
const result = setValueByPath(model, '/user/age', 30);
|
|
122
|
+
expect(result).toEqual({ user: { name: 'John', age: 30 } });
|
|
123
|
+
});
|
|
124
|
+
it('should update existing nested value', () => {
|
|
125
|
+
const model = { user: { name: 'John' } };
|
|
126
|
+
const result = setValueByPath(model, '/user/name', 'Jane');
|
|
127
|
+
expect(result).toEqual({ user: { name: 'Jane' } });
|
|
128
|
+
});
|
|
129
|
+
it('should create nested structure if not exists', () => {
|
|
130
|
+
const model = {};
|
|
131
|
+
const result = setValueByPath(model, '/user/profile/email', 'test@test.com');
|
|
132
|
+
expect(result).toEqual({
|
|
133
|
+
user: { profile: { email: 'test@test.com' } },
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
it('should replace non-object with object when setting nested path', () => {
|
|
137
|
+
const model = { user: 'string' };
|
|
138
|
+
const result = setValueByPath(model, '/user/name', 'John');
|
|
139
|
+
expect(result).toEqual({ user: { name: 'John' } });
|
|
140
|
+
});
|
|
141
|
+
it('should be immutable - not modify original model', () => {
|
|
142
|
+
const model = { user: { name: 'John' } };
|
|
143
|
+
const result = setValueByPath(model, '/user/name', 'Jane');
|
|
144
|
+
expect(model).toEqual({ user: { name: 'John' } });
|
|
145
|
+
expect(result).toEqual({ user: { name: 'Jane' } });
|
|
146
|
+
});
|
|
147
|
+
it('should handle setting null value', () => {
|
|
148
|
+
const model = { user: { name: 'John' } };
|
|
149
|
+
const result = setValueByPath(model, '/user/name', null);
|
|
150
|
+
expect(result).toEqual({ user: { name: null } });
|
|
151
|
+
});
|
|
152
|
+
it('should handle setting array value', () => {
|
|
153
|
+
const model = { items: [] };
|
|
154
|
+
const result = setValueByPath(model, '/items', ['a', 'b']);
|
|
155
|
+
expect(result).toEqual({ items: ['a', 'b'] });
|
|
156
|
+
});
|
|
157
|
+
it('should handle setting object value', () => {
|
|
158
|
+
const model = { user: null };
|
|
159
|
+
const result = setValueByPath(model, '/user', { name: 'John' });
|
|
160
|
+
expect(result).toEqual({ user: { name: 'John' } });
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
describe('mergeAtPath', () => {
|
|
164
|
+
it('should merge at root for empty path', () => {
|
|
165
|
+
const model = { a: 1 };
|
|
166
|
+
const result = mergeAtPath(model, '', { b: 2 });
|
|
167
|
+
expect(result).toEqual({ a: 1, b: 2 });
|
|
168
|
+
});
|
|
169
|
+
it('should merge at root for root path', () => {
|
|
170
|
+
const model = { a: 1 };
|
|
171
|
+
const result = mergeAtPath(model, '/', { b: 2 });
|
|
172
|
+
expect(result).toEqual({ a: 1, b: 2 });
|
|
173
|
+
});
|
|
174
|
+
it('should overwrite existing keys at root', () => {
|
|
175
|
+
const model = { a: 1, b: 2 };
|
|
176
|
+
const result = mergeAtPath(model, '/', { b: 3, c: 4 });
|
|
177
|
+
expect(result).toEqual({ a: 1, b: 3, c: 4 });
|
|
178
|
+
});
|
|
179
|
+
it('should merge at nested path', () => {
|
|
180
|
+
const model = { user: { name: 'John' } };
|
|
181
|
+
const result = mergeAtPath(model, '/user', { age: 30 });
|
|
182
|
+
expect(result).toEqual({ user: { name: 'John', age: 30 } });
|
|
183
|
+
});
|
|
184
|
+
it('should overwrite existing keys at nested path', () => {
|
|
185
|
+
const model = { user: { name: 'John', age: 25 } };
|
|
186
|
+
const result = mergeAtPath(model, '/user', { age: 30 });
|
|
187
|
+
expect(result).toEqual({ user: { name: 'John', age: 30 } });
|
|
188
|
+
});
|
|
189
|
+
it('should create path if not exists', () => {
|
|
190
|
+
const model = {};
|
|
191
|
+
const result = mergeAtPath(model, '/user', { name: 'John' });
|
|
192
|
+
expect(result).toEqual({ user: { name: 'John' } });
|
|
193
|
+
});
|
|
194
|
+
it('should replace non-object at path with merged result', () => {
|
|
195
|
+
const model = { user: 'string' };
|
|
196
|
+
const result = mergeAtPath(model, '/user', { name: 'John' });
|
|
197
|
+
expect(result).toEqual({ user: { name: 'John' } });
|
|
198
|
+
});
|
|
199
|
+
it('should handle array at path by treating as non-object', () => {
|
|
200
|
+
const model = { items: [1, 2, 3] };
|
|
201
|
+
const result = mergeAtPath(model, '/items', { a: 1 });
|
|
202
|
+
expect(result).toEqual({ items: { a: 1 } });
|
|
203
|
+
});
|
|
204
|
+
it('should be immutable - not modify original model', () => {
|
|
205
|
+
const model = { user: { name: 'John' } };
|
|
206
|
+
const result = mergeAtPath(model, '/user', { age: 30 });
|
|
207
|
+
expect(model).toEqual({ user: { name: 'John' } });
|
|
208
|
+
expect(result).toEqual({ user: { name: 'John', age: 30 } });
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data binding utility functions for A2UI 0.9.
|
|
3
|
+
*
|
|
4
|
+
* Handles resolving dynamic values with the simplified 0.9 format:
|
|
5
|
+
* - Literal values: `"string"`, `42`, `true`
|
|
6
|
+
* - Path bindings: `{"path": "/user/name"}` or `{"path": "relative"}`
|
|
7
|
+
* - Function calls: `{"call": "functionName", "args": {...}}`
|
|
8
|
+
*/
|
|
9
|
+
import type { DynamicValue, DynamicString, DataModel, FormBindableValue } from '@a2ui-sdk/types/0.9';
|
|
10
|
+
/**
|
|
11
|
+
* Type guard to check if a value is a path binding.
|
|
12
|
+
*
|
|
13
|
+
* @param value - The value to check
|
|
14
|
+
* @returns True if the value is a path binding object
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* isPathBinding({ path: "/user/name" }); // true
|
|
18
|
+
* isPathBinding("literal"); // false
|
|
19
|
+
* isPathBinding({ call: "required" }); // false
|
|
20
|
+
*/
|
|
21
|
+
export declare function isPathBinding(value: FormBindableValue | undefined | null): value is {
|
|
22
|
+
path: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Type guard to check if a value is a function call.
|
|
26
|
+
*
|
|
27
|
+
* @param value - The value to check
|
|
28
|
+
* @returns True if the value is a function call object
|
|
29
|
+
*/
|
|
30
|
+
export declare function isFunctionCall(value: FormBindableValue | undefined | null): value is {
|
|
31
|
+
call: string;
|
|
32
|
+
args?: Record<string, DynamicValue>;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Resolves a dynamic value to its actual value.
|
|
36
|
+
*
|
|
37
|
+
* Resolution order:
|
|
38
|
+
* 1. If value is a path binding object → resolve from data model
|
|
39
|
+
* 2. If value is a function call → return undefined (function calls not evaluated here)
|
|
40
|
+
* 3. Otherwise → return literal value
|
|
41
|
+
*
|
|
42
|
+
* @param value - The dynamic value (literal, path binding, or function call)
|
|
43
|
+
* @param dataModel - The data model for path lookups
|
|
44
|
+
* @param basePath - Optional base path for relative path resolution
|
|
45
|
+
* @param defaultValue - Default value if undefined or path not found
|
|
46
|
+
* @returns The resolved value
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* // Literal values
|
|
50
|
+
* resolveValue("Hello", {}); // "Hello"
|
|
51
|
+
* resolveValue(42, {}); // 42
|
|
52
|
+
* resolveValue(true, {}); // true
|
|
53
|
+
*
|
|
54
|
+
* // Path bindings
|
|
55
|
+
* const model = { user: { name: "John" } };
|
|
56
|
+
* resolveValue({ path: "/user/name" }, model); // "John"
|
|
57
|
+
* resolveValue({ path: "name" }, model, "/user"); // "John" (relative)
|
|
58
|
+
* resolveValue({ path: "/nonexistent" }, model, null, "default"); // "default"
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolveValue<T = unknown>(value: FormBindableValue | undefined | null, dataModel: DataModel, basePath?: string | null, defaultValue?: T): T;
|
|
61
|
+
/**
|
|
62
|
+
* Resolves a DynamicString value to a string.
|
|
63
|
+
* This is a convenience wrapper for string values.
|
|
64
|
+
*
|
|
65
|
+
* Supports interpolation: if the string contains `${...}` expressions,
|
|
66
|
+
* they will be replaced with values from the data model.
|
|
67
|
+
*
|
|
68
|
+
* @param value - The dynamic string value
|
|
69
|
+
* @param dataModel - The data model for path lookups
|
|
70
|
+
* @param basePath - Optional base path for relative path resolution
|
|
71
|
+
* @param defaultValue - Default value if undefined or path not found
|
|
72
|
+
* @returns The resolved string
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* // Simple string
|
|
76
|
+
* resolveString("Hello", {}); // "Hello"
|
|
77
|
+
*
|
|
78
|
+
* // Path binding
|
|
79
|
+
* resolveString({ path: "/user/name" }, { user: { name: "John" } }); // "John"
|
|
80
|
+
*
|
|
81
|
+
* // Interpolation
|
|
82
|
+
* resolveString("Hello, ${/user/name}!", { user: { name: "John" } }); // "Hello, John!"
|
|
83
|
+
*/
|
|
84
|
+
export declare function resolveString(value: DynamicString | undefined | null, dataModel: DataModel, basePath?: string | null, defaultValue?: string): string;
|
|
85
|
+
/**
|
|
86
|
+
* Resolves action context values to a plain object.
|
|
87
|
+
* All path bindings are resolved to their actual values.
|
|
88
|
+
*
|
|
89
|
+
* @param context - Object with dynamic values
|
|
90
|
+
* @param dataModel - The data model for path lookups
|
|
91
|
+
* @param basePath - Optional base path for relative path resolution
|
|
92
|
+
* @returns A plain object with resolved context values
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* const context = {
|
|
96
|
+
* name: { path: "/user/name" },
|
|
97
|
+
* action: "submit",
|
|
98
|
+
* count: 42
|
|
99
|
+
* };
|
|
100
|
+
* resolveContext(context, { user: { name: "John" } });
|
|
101
|
+
* // Returns: { name: "John", action: "submit", count: 42 }
|
|
102
|
+
*/
|
|
103
|
+
export declare function resolveContext(context: Record<string, DynamicValue> | undefined, dataModel: DataModel, basePath?: string | null): Record<string, unknown>;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data binding utility functions for A2UI 0.9.
|
|
3
|
+
*
|
|
4
|
+
* Handles resolving dynamic values with the simplified 0.9 format:
|
|
5
|
+
* - Literal values: `"string"`, `42`, `true`
|
|
6
|
+
* - Path bindings: `{"path": "/user/name"}` or `{"path": "relative"}`
|
|
7
|
+
* - Function calls: `{"call": "functionName", "args": {...}}`
|
|
8
|
+
*/
|
|
9
|
+
import { getValueByPath, resolvePath } from './pathUtils.js';
|
|
10
|
+
import { interpolate } from '@a2ui-sdk/utils/0.9';
|
|
11
|
+
/**
|
|
12
|
+
* Type guard to check if a value is a path binding.
|
|
13
|
+
*
|
|
14
|
+
* @param value - The value to check
|
|
15
|
+
* @returns True if the value is a path binding object
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* isPathBinding({ path: "/user/name" }); // true
|
|
19
|
+
* isPathBinding("literal"); // false
|
|
20
|
+
* isPathBinding({ call: "required" }); // false
|
|
21
|
+
*/
|
|
22
|
+
export function isPathBinding(value) {
|
|
23
|
+
return (value !== undefined &&
|
|
24
|
+
value !== null &&
|
|
25
|
+
typeof value === 'object' &&
|
|
26
|
+
!Array.isArray(value) &&
|
|
27
|
+
'path' in value &&
|
|
28
|
+
typeof value.path === 'string');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Type guard to check if a value is a function call.
|
|
32
|
+
*
|
|
33
|
+
* @param value - The value to check
|
|
34
|
+
* @returns True if the value is a function call object
|
|
35
|
+
*/
|
|
36
|
+
export function isFunctionCall(value) {
|
|
37
|
+
return (value !== undefined &&
|
|
38
|
+
value !== null &&
|
|
39
|
+
typeof value === 'object' &&
|
|
40
|
+
!Array.isArray(value) &&
|
|
41
|
+
'call' in value &&
|
|
42
|
+
typeof value.call === 'string');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolves a dynamic value to its actual value.
|
|
46
|
+
*
|
|
47
|
+
* Resolution order:
|
|
48
|
+
* 1. If value is a path binding object → resolve from data model
|
|
49
|
+
* 2. If value is a function call → return undefined (function calls not evaluated here)
|
|
50
|
+
* 3. Otherwise → return literal value
|
|
51
|
+
*
|
|
52
|
+
* @param value - The dynamic value (literal, path binding, or function call)
|
|
53
|
+
* @param dataModel - The data model for path lookups
|
|
54
|
+
* @param basePath - Optional base path for relative path resolution
|
|
55
|
+
* @param defaultValue - Default value if undefined or path not found
|
|
56
|
+
* @returns The resolved value
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* // Literal values
|
|
60
|
+
* resolveValue("Hello", {}); // "Hello"
|
|
61
|
+
* resolveValue(42, {}); // 42
|
|
62
|
+
* resolveValue(true, {}); // true
|
|
63
|
+
*
|
|
64
|
+
* // Path bindings
|
|
65
|
+
* const model = { user: { name: "John" } };
|
|
66
|
+
* resolveValue({ path: "/user/name" }, model); // "John"
|
|
67
|
+
* resolveValue({ path: "name" }, model, "/user"); // "John" (relative)
|
|
68
|
+
* resolveValue({ path: "/nonexistent" }, model, null, "default"); // "default"
|
|
69
|
+
*/
|
|
70
|
+
export function resolveValue(value, dataModel, basePath = null, defaultValue) {
|
|
71
|
+
if (value === undefined || value === null) {
|
|
72
|
+
return defaultValue;
|
|
73
|
+
}
|
|
74
|
+
// Path binding
|
|
75
|
+
if (isPathBinding(value)) {
|
|
76
|
+
const resolvedPath = resolvePath(value.path, basePath);
|
|
77
|
+
const result = getValueByPath(dataModel, resolvedPath);
|
|
78
|
+
if (result === undefined) {
|
|
79
|
+
return defaultValue;
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
// Function call - not evaluated here, return default
|
|
84
|
+
if (isFunctionCall(value)) {
|
|
85
|
+
return defaultValue;
|
|
86
|
+
}
|
|
87
|
+
// Literal value
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Resolves a DynamicString value to a string.
|
|
92
|
+
* This is a convenience wrapper for string values.
|
|
93
|
+
*
|
|
94
|
+
* Supports interpolation: if the string contains `${...}` expressions,
|
|
95
|
+
* they will be replaced with values from the data model.
|
|
96
|
+
*
|
|
97
|
+
* @param value - The dynamic string value
|
|
98
|
+
* @param dataModel - The data model for path lookups
|
|
99
|
+
* @param basePath - Optional base path for relative path resolution
|
|
100
|
+
* @param defaultValue - Default value if undefined or path not found
|
|
101
|
+
* @returns The resolved string
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* // Simple string
|
|
105
|
+
* resolveString("Hello", {}); // "Hello"
|
|
106
|
+
*
|
|
107
|
+
* // Path binding
|
|
108
|
+
* resolveString({ path: "/user/name" }, { user: { name: "John" } }); // "John"
|
|
109
|
+
*
|
|
110
|
+
* // Interpolation
|
|
111
|
+
* resolveString("Hello, ${/user/name}!", { user: { name: "John" } }); // "Hello, John!"
|
|
112
|
+
*/
|
|
113
|
+
export function resolveString(value, dataModel, basePath = null, defaultValue = '') {
|
|
114
|
+
// Handle undefined/null
|
|
115
|
+
if (value === undefined || value === null) {
|
|
116
|
+
return defaultValue;
|
|
117
|
+
}
|
|
118
|
+
// Handle path binding
|
|
119
|
+
if (isPathBinding(value)) {
|
|
120
|
+
const resolvedPath = resolvePath(value.path, basePath);
|
|
121
|
+
const result = getValueByPath(dataModel, resolvedPath);
|
|
122
|
+
if (result === undefined || result === null) {
|
|
123
|
+
return defaultValue;
|
|
124
|
+
}
|
|
125
|
+
return String(result);
|
|
126
|
+
}
|
|
127
|
+
// Handle function call (not evaluated here)
|
|
128
|
+
if (isFunctionCall(value)) {
|
|
129
|
+
return defaultValue;
|
|
130
|
+
}
|
|
131
|
+
// Handle string literal - perform interpolation
|
|
132
|
+
if (typeof value === 'string') {
|
|
133
|
+
return interpolate(value, dataModel, basePath);
|
|
134
|
+
}
|
|
135
|
+
// Other types (number, boolean) - convert to string
|
|
136
|
+
return String(value);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Resolves action context values to a plain object.
|
|
140
|
+
* All path bindings are resolved to their actual values.
|
|
141
|
+
*
|
|
142
|
+
* @param context - Object with dynamic values
|
|
143
|
+
* @param dataModel - The data model for path lookups
|
|
144
|
+
* @param basePath - Optional base path for relative path resolution
|
|
145
|
+
* @returns A plain object with resolved context values
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* const context = {
|
|
149
|
+
* name: { path: "/user/name" },
|
|
150
|
+
* action: "submit",
|
|
151
|
+
* count: 42
|
|
152
|
+
* };
|
|
153
|
+
* resolveContext(context, { user: { name: "John" } });
|
|
154
|
+
* // Returns: { name: "John", action: "submit", count: 42 }
|
|
155
|
+
*/
|
|
156
|
+
export function resolveContext(context, dataModel, basePath = null) {
|
|
157
|
+
if (!context) {
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
160
|
+
const result = {};
|
|
161
|
+
for (const [key, value] of Object.entries(context)) {
|
|
162
|
+
result[key] = resolveValue(value, dataModel, basePath);
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|