@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 ADDED
@@ -0,0 +1,154 @@
1
+ # @a2ui-sdk/utils
2
+
3
+ Utility functions for working with A2UI protocol. This package provides helpers for data binding resolution, path manipulation, string interpolation, and validation.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @a2ui-sdk/utils
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### v0.9 (Latest)
14
+
15
+ ```tsx
16
+ import {
17
+ // String interpolation
18
+ hasInterpolation,
19
+ interpolate,
20
+
21
+ // Data binding utilities
22
+ resolveValue,
23
+ resolveStringValue,
24
+ resolveNumberValue,
25
+ resolveBooleanValue,
26
+ isPathBinding,
27
+
28
+ // Path utilities
29
+ parsePath,
30
+ joinPath,
31
+ getValueAtPath,
32
+ setValueAtPath,
33
+
34
+ // Validation utilities
35
+ evaluateCheckRule,
36
+ validateChecks,
37
+ } from '@a2ui-sdk/utils/0.9'
38
+ ```
39
+
40
+ ### v0.8
41
+
42
+ ```tsx
43
+ import {
44
+ // Data binding utilities
45
+ resolveValue,
46
+ resolveStringValue,
47
+ resolveNumberValue,
48
+ resolveBooleanValue,
49
+ isPathBinding,
50
+
51
+ // Path utilities
52
+ parsePath,
53
+ joinPath,
54
+ getValueAtPath,
55
+ setValueAtPath,
56
+ } from '@a2ui-sdk/utils/0.8'
57
+ ```
58
+
59
+ ### Namespace Import
60
+
61
+ ```tsx
62
+ import { v0_8, v0_9 } from '@a2ui-sdk/utils'
63
+
64
+ // Use v0.9 utilities
65
+ const { interpolate, hasInterpolation } = v0_9
66
+ ```
67
+
68
+ ## API
69
+
70
+ ### String Interpolation (v0.9)
71
+
72
+ Process strings with embedded expressions:
73
+
74
+ ```tsx
75
+ import { hasInterpolation, interpolate } from '@a2ui-sdk/utils/0.9'
76
+
77
+ // Check if string contains interpolation
78
+ hasInterpolation('Hello {{name}}') // true
79
+ hasInterpolation('Hello world') // false
80
+
81
+ // Interpolate string with data
82
+ const data = { name: 'Alice', count: 5 }
83
+ interpolate('Hello {{name}}, you have {{count}} items', data)
84
+ // => 'Hello Alice, you have 5 items'
85
+ ```
86
+
87
+ ### Data Binding
88
+
89
+ Resolve dynamic values from the data model:
90
+
91
+ ```tsx
92
+ import { resolveValue, isPathBinding } from '@a2ui-sdk/utils/0.9'
93
+
94
+ const dataModel = { user: { name: 'Alice' } }
95
+
96
+ // Check if value is a path binding
97
+ isPathBinding({ path: '/user/name' }) // true
98
+ isPathBinding('static value') // false
99
+
100
+ // Resolve value
101
+ resolveValue({ path: '/user/name' }, dataModel) // 'Alice'
102
+ resolveValue('static value', dataModel) // 'static value'
103
+ ```
104
+
105
+ ### Path Utilities
106
+
107
+ Work with JSON Pointer paths (RFC 6901):
108
+
109
+ ```tsx
110
+ import {
111
+ parsePath,
112
+ joinPath,
113
+ getValueAtPath,
114
+ setValueAtPath,
115
+ } from '@a2ui-sdk/utils/0.9'
116
+
117
+ // Parse path into segments
118
+ parsePath('/user/profile/name') // ['user', 'profile', 'name']
119
+
120
+ // Join segments into path
121
+ joinPath(['user', 'profile', 'name']) // '/user/profile/name'
122
+
123
+ // Get value at path
124
+ const data = { user: { profile: { name: 'Alice' } } }
125
+ getValueAtPath(data, '/user/profile/name') // 'Alice'
126
+
127
+ // Set value at path (immutable)
128
+ const newData = setValueAtPath(data, '/user/profile/name', 'Bob')
129
+ // data is unchanged, newData has the update
130
+ ```
131
+
132
+ ### Validation (v0.9)
133
+
134
+ Evaluate validation rules:
135
+
136
+ ```tsx
137
+ import { evaluateCheckRule, validateChecks } from '@a2ui-sdk/utils/0.9'
138
+
139
+ const checks = [
140
+ {
141
+ message: 'Name is required',
142
+ call: 'isNotEmpty',
143
+ args: { value: { path: '/form/name' } },
144
+ },
145
+ ]
146
+
147
+ const dataModel = { form: { name: '' } }
148
+ const result = validateChecks(checks, dataModel)
149
+ // { valid: false, errors: ['Name is required'] }
150
+ ```
151
+
152
+ ## License
153
+
154
+ Apache-2.0
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Data binding utility functions for A2UI.
3
+ * Handles resolving value sources and converting data entries.
4
+ */
5
+ import type { ValueSource, DataModel, DataEntry, DataModelValue } from '@a2ui-sdk/types/0.8';
6
+ /**
7
+ * Resolves a ValueSource to its actual value.
8
+ *
9
+ * @param source - The value source (literal or path reference)
10
+ * @param dataModel - The data model for path lookups
11
+ * @param defaultValue - Default value if source is undefined or path not found
12
+ * @returns The resolved value
13
+ *
14
+ * @example
15
+ * // Literal values
16
+ * resolveValue({ literalString: "Hello" }, {}); // "Hello"
17
+ * resolveValue({ literalNumber: 42 }, {}); // 42
18
+ *
19
+ * // Path references
20
+ * const model = { user: { name: "John" } };
21
+ * resolveValue({ path: "/user/name" }, model); // "John"
22
+ * resolveValue({ path: "/user/age" }, model, 0); // 0 (default)
23
+ */
24
+ export declare function resolveValue<T = unknown>(source: ValueSource | undefined, dataModel: DataModel, defaultValue?: T): T;
25
+ /**
26
+ * Converts a DataEntry array to a plain object.
27
+ * This is used for processing dataModelUpdate message contents.
28
+ *
29
+ * @param contents - Array of data entries from the server
30
+ * @returns A plain object with the converted values
31
+ *
32
+ * @example
33
+ * contentsToObject([
34
+ * { key: "name", valueString: "John" },
35
+ * { key: "age", valueNumber: 30 },
36
+ * { key: "active", valueBoolean: true },
37
+ * { key: "profile", valueMap: [
38
+ * { key: "email", valueString: "john@example.com" }
39
+ * ]}
40
+ * ]);
41
+ * // Returns: { name: "John", age: 30, active: true, profile: { email: "john@example.com" } }
42
+ */
43
+ export declare function contentsToObject(contents: DataEntry[]): Record<string, DataModelValue>;
44
+ /**
45
+ * Resolves action context items to a plain object.
46
+ * This is used when dispatching actions to resolve all context values.
47
+ *
48
+ * @param context - Array of action context items
49
+ * @param dataModel - The data model for path lookups
50
+ * @returns A plain object with resolved context values
51
+ */
52
+ export declare function resolveActionContext(context: Array<{
53
+ key: string;
54
+ value: ValueSource;
55
+ }> | undefined, dataModel: DataModel): Record<string, unknown>;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Data binding utility functions for A2UI.
3
+ * Handles resolving value sources and converting data entries.
4
+ */
5
+ import { getValueByPath } from './pathUtils.js';
6
+ /**
7
+ * Resolves a ValueSource to its actual value.
8
+ *
9
+ * @param source - The value source (literal or path reference)
10
+ * @param dataModel - The data model for path lookups
11
+ * @param defaultValue - Default value if source is undefined or path not found
12
+ * @returns The resolved value
13
+ *
14
+ * @example
15
+ * // Literal values
16
+ * resolveValue({ literalString: "Hello" }, {}); // "Hello"
17
+ * resolveValue({ literalNumber: 42 }, {}); // 42
18
+ *
19
+ * // Path references
20
+ * const model = { user: { name: "John" } };
21
+ * resolveValue({ path: "/user/name" }, model); // "John"
22
+ * resolveValue({ path: "/user/age" }, model, 0); // 0 (default)
23
+ */
24
+ export function resolveValue(source, dataModel, defaultValue) {
25
+ if (source === undefined || source === null) {
26
+ return defaultValue;
27
+ }
28
+ if ('literalString' in source) {
29
+ return source.literalString;
30
+ }
31
+ if ('literalNumber' in source) {
32
+ return source.literalNumber;
33
+ }
34
+ if ('literalBoolean' in source) {
35
+ return source.literalBoolean;
36
+ }
37
+ if ('literalArray' in source) {
38
+ return source.literalArray;
39
+ }
40
+ if ('path' in source) {
41
+ const value = getValueByPath(dataModel, source.path);
42
+ if (value === undefined) {
43
+ return defaultValue;
44
+ }
45
+ return value;
46
+ }
47
+ return defaultValue;
48
+ }
49
+ /**
50
+ * Converts a DataEntry array to a plain object.
51
+ * This is used for processing dataModelUpdate message contents.
52
+ *
53
+ * @param contents - Array of data entries from the server
54
+ * @returns A plain object with the converted values
55
+ *
56
+ * @example
57
+ * contentsToObject([
58
+ * { key: "name", valueString: "John" },
59
+ * { key: "age", valueNumber: 30 },
60
+ * { key: "active", valueBoolean: true },
61
+ * { key: "profile", valueMap: [
62
+ * { key: "email", valueString: "john@example.com" }
63
+ * ]}
64
+ * ]);
65
+ * // Returns: { name: "John", age: 30, active: true, profile: { email: "john@example.com" } }
66
+ */
67
+ export function contentsToObject(contents) {
68
+ const result = {};
69
+ for (const entry of contents) {
70
+ const key = normalizeKey(entry.key);
71
+ if (entry.valueString !== undefined) {
72
+ result[key] = entry.valueString;
73
+ }
74
+ else if (entry.valueNumber !== undefined) {
75
+ result[key] = entry.valueNumber;
76
+ }
77
+ else if (entry.valueBoolean !== undefined) {
78
+ result[key] = entry.valueBoolean;
79
+ }
80
+ else if (entry.valueMap !== undefined) {
81
+ result[key] = contentsToObject(entry.valueMap);
82
+ }
83
+ }
84
+ return result;
85
+ }
86
+ /**
87
+ * Normalizes a key from the data entry format.
88
+ * Keys can come as "/form/name" or just "name".
89
+ *
90
+ * @param key - The key to normalize
91
+ * @returns The normalized key (last segment)
92
+ */
93
+ function normalizeKey(key) {
94
+ // If key contains path separators, take the last segment
95
+ if (key.includes('/')) {
96
+ const segments = key.split('/').filter(Boolean);
97
+ return segments[segments.length - 1] || key;
98
+ }
99
+ return key;
100
+ }
101
+ /**
102
+ * Resolves action context items to a plain object.
103
+ * This is used when dispatching actions to resolve all context values.
104
+ *
105
+ * @param context - Array of action context items
106
+ * @param dataModel - The data model for path lookups
107
+ * @returns A plain object with resolved context values
108
+ */
109
+ export function resolveActionContext(context, dataModel) {
110
+ if (!context) {
111
+ return {};
112
+ }
113
+ const result = {};
114
+ for (const item of context) {
115
+ result[item.key] = resolveValue(item.value, dataModel);
116
+ }
117
+ return result;
118
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * dataBinding Tests
3
+ *
4
+ * Tests for data binding utility functions used in A2UI.
5
+ */
6
+ export {};
@@ -0,0 +1,271 @@
1
+ /**
2
+ * dataBinding Tests
3
+ *
4
+ * Tests for data binding utility functions used in A2UI.
5
+ */
6
+ import { describe, it, expect } from 'vitest';
7
+ import { resolveValue, contentsToObject, resolveActionContext, } from './dataBinding.js';
8
+ describe('dataBinding', () => {
9
+ describe('resolveValue', () => {
10
+ const testModel = {
11
+ user: {
12
+ name: 'John',
13
+ age: 30,
14
+ active: true,
15
+ },
16
+ items: ['a', 'b', 'c'],
17
+ count: 42,
18
+ };
19
+ describe('with undefined/null source', () => {
20
+ it('should return default value when source is undefined', () => {
21
+ expect(resolveValue(undefined, testModel, 'default')).toBe('default');
22
+ });
23
+ it('should return default value when source is null', () => {
24
+ expect(resolveValue(null, testModel, 'default')).toBe('default');
25
+ });
26
+ it('should return undefined when no default provided and source is undefined', () => {
27
+ expect(resolveValue(undefined, testModel)).toBeUndefined();
28
+ });
29
+ });
30
+ describe('with literal values', () => {
31
+ it('should resolve literalString', () => {
32
+ const source = { literalString: 'Hello' };
33
+ expect(resolveValue(source, testModel)).toBe('Hello');
34
+ });
35
+ it('should resolve empty literalString', () => {
36
+ const source = { literalString: '' };
37
+ expect(resolveValue(source, testModel, 'default')).toBe('');
38
+ });
39
+ it('should resolve literalNumber', () => {
40
+ const source = { literalNumber: 42 };
41
+ expect(resolveValue(source, testModel)).toBe(42);
42
+ });
43
+ it('should resolve zero literalNumber', () => {
44
+ const source = { literalNumber: 0 };
45
+ expect(resolveValue(source, testModel, 99)).toBe(0);
46
+ });
47
+ it('should resolve negative literalNumber', () => {
48
+ const source = { literalNumber: -10 };
49
+ expect(resolveValue(source, testModel)).toBe(-10);
50
+ });
51
+ it('should resolve literalBoolean true', () => {
52
+ const source = { literalBoolean: true };
53
+ expect(resolveValue(source, testModel)).toBe(true);
54
+ });
55
+ it('should resolve literalBoolean false', () => {
56
+ const source = { literalBoolean: false };
57
+ expect(resolveValue(source, testModel, true)).toBe(false);
58
+ });
59
+ it('should resolve literalArray', () => {
60
+ const source = { literalArray: ['x', 'y', 'z'] };
61
+ expect(resolveValue(source, testModel)).toEqual([
62
+ 'x',
63
+ 'y',
64
+ 'z',
65
+ ]);
66
+ });
67
+ it('should resolve empty literalArray', () => {
68
+ const source = { literalArray: [] };
69
+ expect(resolveValue(source, testModel, ['default'])).toEqual([]);
70
+ });
71
+ });
72
+ describe('with path references', () => {
73
+ it('should resolve path to string value', () => {
74
+ const source = { path: '/user/name' };
75
+ expect(resolveValue(source, testModel)).toBe('John');
76
+ });
77
+ it('should resolve path to number value', () => {
78
+ const source = { path: '/count' };
79
+ expect(resolveValue(source, testModel)).toBe(42);
80
+ });
81
+ it('should resolve path to boolean value', () => {
82
+ const source = { path: '/user/active' };
83
+ expect(resolveValue(source, testModel)).toBe(true);
84
+ });
85
+ it('should resolve path to nested object', () => {
86
+ const source = { path: '/user' };
87
+ expect(resolveValue(source, testModel)).toEqual({
88
+ name: 'John',
89
+ age: 30,
90
+ active: true,
91
+ });
92
+ });
93
+ it('should resolve path to array', () => {
94
+ const source = { path: '/items' };
95
+ expect(resolveValue(source, testModel)).toEqual([
96
+ 'a',
97
+ 'b',
98
+ 'c',
99
+ ]);
100
+ });
101
+ it('should return default when path not found', () => {
102
+ const source = { path: '/nonexistent' };
103
+ expect(resolveValue(source, testModel, 'default')).toBe('default');
104
+ });
105
+ it('should return undefined when path not found and no default', () => {
106
+ const source = { path: '/nonexistent' };
107
+ expect(resolveValue(source, testModel)).toBeUndefined();
108
+ });
109
+ it('should handle empty data model', () => {
110
+ const source = { path: '/user/name' };
111
+ expect(resolveValue(source, {}, 'default')).toBe('default');
112
+ });
113
+ });
114
+ describe('with unknown source structure', () => {
115
+ it('should return default value for unknown source structure', () => {
116
+ const source = { unknown: 'value' };
117
+ expect(resolveValue(source, testModel, 'default')).toBe('default');
118
+ });
119
+ });
120
+ });
121
+ describe('contentsToObject', () => {
122
+ it('should convert string entry', () => {
123
+ const contents = [{ key: 'name', valueString: 'John' }];
124
+ expect(contentsToObject(contents)).toEqual({ name: 'John' });
125
+ });
126
+ it('should convert number entry', () => {
127
+ const contents = [{ key: 'age', valueNumber: 30 }];
128
+ expect(contentsToObject(contents)).toEqual({ age: 30 });
129
+ });
130
+ it('should convert boolean entry', () => {
131
+ const contents = [{ key: 'active', valueBoolean: true }];
132
+ expect(contentsToObject(contents)).toEqual({ active: true });
133
+ });
134
+ it('should convert false boolean entry', () => {
135
+ const contents = [{ key: 'active', valueBoolean: false }];
136
+ expect(contentsToObject(contents)).toEqual({ active: false });
137
+ });
138
+ it('should convert zero number entry', () => {
139
+ const contents = [{ key: 'count', valueNumber: 0 }];
140
+ expect(contentsToObject(contents)).toEqual({ count: 0 });
141
+ });
142
+ it('should convert empty string entry', () => {
143
+ const contents = [{ key: 'name', valueString: '' }];
144
+ expect(contentsToObject(contents)).toEqual({ name: '' });
145
+ });
146
+ it('should convert nested map entry', () => {
147
+ const contents = [
148
+ {
149
+ key: 'user',
150
+ valueMap: [
151
+ { key: 'name', valueString: 'John' },
152
+ { key: 'age', valueNumber: 30 },
153
+ ],
154
+ },
155
+ ];
156
+ expect(contentsToObject(contents)).toEqual({
157
+ user: { name: 'John', age: 30 },
158
+ });
159
+ });
160
+ it('should convert deeply nested map entry', () => {
161
+ const contents = [
162
+ {
163
+ key: 'user',
164
+ valueMap: [
165
+ {
166
+ key: 'profile',
167
+ valueMap: [{ key: 'email', valueString: 'john@example.com' }],
168
+ },
169
+ ],
170
+ },
171
+ ];
172
+ expect(contentsToObject(contents)).toEqual({
173
+ user: { profile: { email: 'john@example.com' } },
174
+ });
175
+ });
176
+ it('should convert multiple entries', () => {
177
+ const contents = [
178
+ { key: 'name', valueString: 'John' },
179
+ { key: 'age', valueNumber: 30 },
180
+ { key: 'active', valueBoolean: true },
181
+ ];
182
+ expect(contentsToObject(contents)).toEqual({
183
+ name: 'John',
184
+ age: 30,
185
+ active: true,
186
+ });
187
+ });
188
+ it('should normalize path keys to last segment', () => {
189
+ const contents = [
190
+ { key: '/form/name', valueString: 'John' },
191
+ { key: '/form/age', valueNumber: 30 },
192
+ ];
193
+ expect(contentsToObject(contents)).toEqual({
194
+ name: 'John',
195
+ age: 30,
196
+ });
197
+ });
198
+ it('should handle empty contents array', () => {
199
+ expect(contentsToObject([])).toEqual({});
200
+ });
201
+ it('should handle entry with no value type', () => {
202
+ const contents = [{ key: 'empty' }];
203
+ expect(contentsToObject(contents)).toEqual({});
204
+ });
205
+ });
206
+ describe('resolveActionContext', () => {
207
+ const testModel = {
208
+ user: {
209
+ name: 'John',
210
+ age: 30,
211
+ },
212
+ selectedId: 'item-123',
213
+ };
214
+ it('should return empty object for undefined context', () => {
215
+ expect(resolveActionContext(undefined, testModel)).toEqual({});
216
+ });
217
+ it('should return empty object for empty context array', () => {
218
+ expect(resolveActionContext([], testModel)).toEqual({});
219
+ });
220
+ it('should resolve literalString values', () => {
221
+ const context = [{ key: 'action', value: { literalString: 'submit' } }];
222
+ expect(resolveActionContext(context, testModel)).toEqual({
223
+ action: 'submit',
224
+ });
225
+ });
226
+ it('should resolve literalNumber values', () => {
227
+ const context = [{ key: 'count', value: { literalNumber: 5 } }];
228
+ expect(resolveActionContext(context, testModel)).toEqual({ count: 5 });
229
+ });
230
+ it('should resolve literalBoolean values', () => {
231
+ const context = [{ key: 'confirmed', value: { literalBoolean: true } }];
232
+ expect(resolveActionContext(context, testModel)).toEqual({
233
+ confirmed: true,
234
+ });
235
+ });
236
+ it('should resolve path references', () => {
237
+ const context = [{ key: 'userName', value: { path: '/user/name' } }];
238
+ expect(resolveActionContext(context, testModel)).toEqual({
239
+ userName: 'John',
240
+ });
241
+ });
242
+ it('should resolve multiple context items', () => {
243
+ const context = [
244
+ { key: 'action', value: { literalString: 'update' } },
245
+ { key: 'userId', value: { path: '/selectedId' } },
246
+ { key: 'confirmed', value: { literalBoolean: true } },
247
+ ];
248
+ expect(resolveActionContext(context, testModel)).toEqual({
249
+ action: 'update',
250
+ userId: 'item-123',
251
+ confirmed: true,
252
+ });
253
+ });
254
+ it('should return undefined for non-existent path', () => {
255
+ const context = [{ key: 'missing', value: { path: '/nonexistent' } }];
256
+ expect(resolveActionContext(context, testModel)).toEqual({
257
+ missing: undefined,
258
+ });
259
+ });
260
+ it('should resolve nested path references', () => {
261
+ const context = [
262
+ { key: 'name', value: { path: '/user/name' } },
263
+ { key: 'age', value: { path: '/user/age' } },
264
+ ];
265
+ expect(resolveActionContext(context, testModel)).toEqual({
266
+ name: 'John',
267
+ age: 30,
268
+ });
269
+ });
270
+ });
271
+ });
@@ -0,0 +1,2 @@
1
+ export * from './dataBinding.js';
2
+ export * from './pathUtils.js';
@@ -0,0 +1,2 @@
1
+ export * from './dataBinding.js';
2
+ export * from './pathUtils.js';
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Path utility functions for A2UI data model operations.
3
+ */
4
+ import type { DataModel, DataModelValue } from '@a2ui-sdk/types/0.8';
5
+ /**
6
+ * Gets a value from the data model by path.
7
+ *
8
+ * @param dataModel - The data model to read from
9
+ * @param path - The path to the value (e.g., "/user/name")
10
+ * @returns The value at the path, or undefined if not found
11
+ *
12
+ * @example
13
+ * const model = { user: { name: "John" } };
14
+ * getValueByPath(model, "/user/name"); // "John"
15
+ * getValueByPath(model, "/user/age"); // undefined
16
+ */
17
+ export declare function getValueByPath(dataModel: DataModel, path: string): DataModelValue | undefined;
18
+ /**
19
+ * Sets a value in the data model by path, returning a new data model.
20
+ * This function is immutable - it does not modify the original data model.
21
+ *
22
+ * @param dataModel - The data model to update
23
+ * @param path - The path to set (e.g., "/user/name")
24
+ * @param value - The value to set
25
+ * @returns A new data model with the value set
26
+ *
27
+ * @example
28
+ * const model = { user: { name: "John" } };
29
+ * setValueByPath(model, "/user/name", "Jane");
30
+ * // Returns: { user: { name: "Jane" } }
31
+ */
32
+ export declare function setValueByPath(dataModel: DataModel, path: string, value: unknown): DataModel;
33
+ /**
34
+ * Merges data into the data model at a given path.
35
+ * This is used for dataModelUpdate messages where contents are merged.
36
+ *
37
+ * @param dataModel - The data model to update
38
+ * @param path - The path to merge at (e.g., "/form")
39
+ * @param data - The data to merge
40
+ * @returns A new data model with the data merged
41
+ */
42
+ export declare function mergeAtPath(dataModel: DataModel, path: string, data: Record<string, unknown>): DataModel;