@a2ui-sdk/utils 0.0.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.
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Path utility functions for A2UI 0.9 data model operations.
3
+ *
4
+ * Implements JSON Pointer (RFC 6901) parsing and resolution with
5
+ * support for relative paths within collection scopes.
6
+ */
7
+ import type { DataModel } from '@a2ui-sdk/types/0.9';
8
+ /**
9
+ * Parses a JSON Pointer path into segments.
10
+ *
11
+ * @param path - The JSON Pointer path (e.g., "/user/name")
12
+ * @returns Array of path segments
13
+ *
14
+ * @example
15
+ * parseJsonPointer("/user/name"); // ["user", "name"]
16
+ * parseJsonPointer("/items/0"); // ["items", "0"]
17
+ * parseJsonPointer("/"); // []
18
+ * parseJsonPointer("/a~1b"); // ["a/b"] (escaped slash)
19
+ * parseJsonPointer("/m~0n"); // ["m~n"] (escaped tilde)
20
+ */
21
+ export declare function parseJsonPointer(path: string): string[];
22
+ /**
23
+ * Creates a JSON Pointer path from segments.
24
+ *
25
+ * @param segments - Array of path segments
26
+ * @returns JSON Pointer path string
27
+ *
28
+ * @example
29
+ * createJsonPointer(["user", "name"]); // "/user/name"
30
+ * createJsonPointer(["a/b"]); // "/a~1b"
31
+ * createJsonPointer([]); // "/"
32
+ */
33
+ export declare function createJsonPointer(segments: string[]): string;
34
+ /**
35
+ * Gets a value from the data model by JSON Pointer path.
36
+ *
37
+ * @param dataModel - The data model to read from
38
+ * @param path - The JSON Pointer path (e.g., "/user/name")
39
+ * @returns The value at the path, or undefined if not found
40
+ *
41
+ * @example
42
+ * const model = { user: { name: "John" }, items: ["a", "b"] };
43
+ * getValueByPath(model, "/user/name"); // "John"
44
+ * getValueByPath(model, "/items/0"); // "a"
45
+ * getValueByPath(model, "/nonexistent"); // undefined
46
+ */
47
+ export declare function getValueByPath(dataModel: DataModel, path: string): unknown;
48
+ /**
49
+ * Sets a value in the data model by JSON Pointer path, returning a new data model.
50
+ * This function is immutable - it does not modify the original data model.
51
+ *
52
+ * @param dataModel - The data model to update
53
+ * @param path - The JSON Pointer path (e.g., "/user/name")
54
+ * @param value - The value to set (undefined to delete)
55
+ * @returns A new data model with the value set
56
+ *
57
+ * @example
58
+ * const model = { user: { name: "John" } };
59
+ * setValueByPath(model, "/user/name", "Jane"); // { user: { name: "Jane" } }
60
+ * setValueByPath(model, "/user/age", 30); // { user: { name: "John", age: 30 } }
61
+ * setValueByPath(model, "/user/name", undefined); // { user: {} }
62
+ */
63
+ export declare function setValueByPath(dataModel: DataModel, path: string, value: unknown): DataModel;
64
+ /**
65
+ * Normalizes a path to ensure it starts with "/" and has no trailing "/".
66
+ *
67
+ * @param path - The path to normalize
68
+ * @returns The normalized path
69
+ *
70
+ * @example
71
+ * normalizePath("user/name"); // "/user/name"
72
+ * normalizePath("/user/name/"); // "/user/name"
73
+ */
74
+ export declare function normalizePath(path: string): string;
75
+ /**
76
+ * Checks if a path is absolute (starts with "/").
77
+ *
78
+ * @param path - The path to check
79
+ * @returns True if the path is absolute
80
+ *
81
+ * @example
82
+ * isAbsolutePath("/user/name"); // true
83
+ * isAbsolutePath("name"); // false
84
+ * isAbsolutePath(""); // false
85
+ */
86
+ export declare function isAbsolutePath(path: string): boolean;
87
+ /**
88
+ * Resolves a path against a base path (scope).
89
+ *
90
+ * Absolute paths (starting with "/") are returned as-is.
91
+ * Relative paths are resolved against the base path.
92
+ *
93
+ * @param path - The path to resolve
94
+ * @param basePath - The base path (scope), or null for root scope
95
+ * @returns The resolved absolute path
96
+ *
97
+ * @example
98
+ * resolvePath("/user/name", "/items/0"); // "/user/name" (absolute)
99
+ * resolvePath("name", "/items/0"); // "/items/0/name" (relative)
100
+ * resolvePath("name", null); // "/name" (root scope)
101
+ */
102
+ export declare function resolvePath(path: string, basePath: string | null): string;
103
+ /**
104
+ * Joins two paths together.
105
+ *
106
+ * @param basePath - The base path
107
+ * @param relativePath - The relative path to join
108
+ * @returns The joined path
109
+ *
110
+ * @example
111
+ * joinPaths("/user", "name"); // "/user/name"
112
+ * joinPaths("/user", "/name"); // "/user/name"
113
+ * joinPaths("/user/", "/name/"); // "/user/name"
114
+ */
115
+ export declare function joinPaths(basePath: string, relativePath: string): string;
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Path utility functions for A2UI 0.9 data model operations.
3
+ *
4
+ * Implements JSON Pointer (RFC 6901) parsing and resolution with
5
+ * support for relative paths within collection scopes.
6
+ */
7
+ /**
8
+ * Parses a JSON Pointer path into segments.
9
+ *
10
+ * @param path - The JSON Pointer path (e.g., "/user/name")
11
+ * @returns Array of path segments
12
+ *
13
+ * @example
14
+ * parseJsonPointer("/user/name"); // ["user", "name"]
15
+ * parseJsonPointer("/items/0"); // ["items", "0"]
16
+ * parseJsonPointer("/"); // []
17
+ * parseJsonPointer("/a~1b"); // ["a/b"] (escaped slash)
18
+ * parseJsonPointer("/m~0n"); // ["m~n"] (escaped tilde)
19
+ */
20
+ export function parseJsonPointer(path) {
21
+ if (!path || path === '/') {
22
+ return [];
23
+ }
24
+ // Remove leading slash and split
25
+ const segments = path.startsWith('/')
26
+ ? path.slice(1).split('/')
27
+ : path.split('/');
28
+ // Unescape JSON Pointer special characters (~1 -> /, ~0 -> ~)
29
+ return segments
30
+ .filter((s) => s !== '')
31
+ .map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~'));
32
+ }
33
+ /**
34
+ * Creates a JSON Pointer path from segments.
35
+ *
36
+ * @param segments - Array of path segments
37
+ * @returns JSON Pointer path string
38
+ *
39
+ * @example
40
+ * createJsonPointer(["user", "name"]); // "/user/name"
41
+ * createJsonPointer(["a/b"]); // "/a~1b"
42
+ * createJsonPointer([]); // "/"
43
+ */
44
+ export function createJsonPointer(segments) {
45
+ if (segments.length === 0) {
46
+ return '/';
47
+ }
48
+ // Escape special characters (~ -> ~0, / -> ~1)
49
+ const escaped = segments.map((segment) => segment.replace(/~/g, '~0').replace(/\//g, '~1'));
50
+ return '/' + escaped.join('/');
51
+ }
52
+ /**
53
+ * Gets a value from the data model by JSON Pointer path.
54
+ *
55
+ * @param dataModel - The data model to read from
56
+ * @param path - The JSON Pointer path (e.g., "/user/name")
57
+ * @returns The value at the path, or undefined if not found
58
+ *
59
+ * @example
60
+ * const model = { user: { name: "John" }, items: ["a", "b"] };
61
+ * getValueByPath(model, "/user/name"); // "John"
62
+ * getValueByPath(model, "/items/0"); // "a"
63
+ * getValueByPath(model, "/nonexistent"); // undefined
64
+ */
65
+ export function getValueByPath(dataModel, path) {
66
+ if (!path || path === '/') {
67
+ return dataModel;
68
+ }
69
+ const segments = parseJsonPointer(path);
70
+ let current = dataModel;
71
+ for (const segment of segments) {
72
+ if (current === null || current === undefined) {
73
+ return undefined;
74
+ }
75
+ if (Array.isArray(current)) {
76
+ const index = parseInt(segment, 10);
77
+ if (isNaN(index)) {
78
+ return undefined;
79
+ }
80
+ current = current[index];
81
+ }
82
+ else if (typeof current === 'object') {
83
+ current = current[segment];
84
+ }
85
+ else {
86
+ return undefined;
87
+ }
88
+ }
89
+ return current;
90
+ }
91
+ /**
92
+ * Sets a value in the data model by JSON Pointer path, returning a new data model.
93
+ * This function is immutable - it does not modify the original data model.
94
+ *
95
+ * @param dataModel - The data model to update
96
+ * @param path - The JSON Pointer path (e.g., "/user/name")
97
+ * @param value - The value to set (undefined to delete)
98
+ * @returns A new data model with the value set
99
+ *
100
+ * @example
101
+ * const model = { user: { name: "John" } };
102
+ * setValueByPath(model, "/user/name", "Jane"); // { user: { name: "Jane" } }
103
+ * setValueByPath(model, "/user/age", 30); // { user: { name: "John", age: 30 } }
104
+ * setValueByPath(model, "/user/name", undefined); // { user: {} }
105
+ */
106
+ export function setValueByPath(dataModel, path, value) {
107
+ // Replace entire data model
108
+ if (!path || path === '/') {
109
+ if (value === undefined) {
110
+ return {};
111
+ }
112
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
113
+ return value;
114
+ }
115
+ return dataModel;
116
+ }
117
+ const segments = parseJsonPointer(path);
118
+ // Deep clone to ensure immutability
119
+ const result = structuredClone(dataModel);
120
+ // Navigate to parent
121
+ let current = result;
122
+ for (let i = 0; i < segments.length - 1; i++) {
123
+ const segment = segments[i];
124
+ if (Array.isArray(current)) {
125
+ const index = parseInt(segment, 10);
126
+ if (isNaN(index)) {
127
+ return result;
128
+ }
129
+ // Ensure array element exists as object
130
+ if (current[index] === null || current[index] === undefined) {
131
+ current[index] = {};
132
+ }
133
+ current = current[index];
134
+ }
135
+ else if (typeof current === 'object' && current !== null) {
136
+ const obj = current;
137
+ // Create intermediate object if needed
138
+ if (obj[segment] === null || obj[segment] === undefined) {
139
+ obj[segment] = {};
140
+ }
141
+ else if (typeof obj[segment] !== 'object') {
142
+ obj[segment] = {};
143
+ }
144
+ current = obj[segment];
145
+ }
146
+ else {
147
+ return result;
148
+ }
149
+ }
150
+ // Set or delete at final segment
151
+ const lastSegment = segments[segments.length - 1];
152
+ if (Array.isArray(current)) {
153
+ const index = parseInt(lastSegment, 10);
154
+ if (!isNaN(index)) {
155
+ if (value === undefined) {
156
+ current.splice(index, 1);
157
+ }
158
+ else {
159
+ current[index] = value;
160
+ }
161
+ }
162
+ }
163
+ else if (typeof current === 'object' && current !== null) {
164
+ const obj = current;
165
+ if (value === undefined) {
166
+ delete obj[lastSegment];
167
+ }
168
+ else {
169
+ obj[lastSegment] = value;
170
+ }
171
+ }
172
+ return result;
173
+ }
174
+ /**
175
+ * Normalizes a path to ensure it starts with "/" and has no trailing "/".
176
+ *
177
+ * @param path - The path to normalize
178
+ * @returns The normalized path
179
+ *
180
+ * @example
181
+ * normalizePath("user/name"); // "/user/name"
182
+ * normalizePath("/user/name/"); // "/user/name"
183
+ */
184
+ export function normalizePath(path) {
185
+ let normalized = path.trim();
186
+ // Ensure starts with /
187
+ if (!normalized.startsWith('/')) {
188
+ normalized = '/' + normalized;
189
+ }
190
+ // Remove trailing / (except for root path)
191
+ if (normalized.length > 1 && normalized.endsWith('/')) {
192
+ normalized = normalized.slice(0, -1);
193
+ }
194
+ return normalized;
195
+ }
196
+ /**
197
+ * Checks if a path is absolute (starts with "/").
198
+ *
199
+ * @param path - The path to check
200
+ * @returns True if the path is absolute
201
+ *
202
+ * @example
203
+ * isAbsolutePath("/user/name"); // true
204
+ * isAbsolutePath("name"); // false
205
+ * isAbsolutePath(""); // false
206
+ */
207
+ export function isAbsolutePath(path) {
208
+ return path.startsWith('/');
209
+ }
210
+ /**
211
+ * Resolves a path against a base path (scope).
212
+ *
213
+ * Absolute paths (starting with "/") are returned as-is.
214
+ * Relative paths are resolved against the base path.
215
+ *
216
+ * @param path - The path to resolve
217
+ * @param basePath - The base path (scope), or null for root scope
218
+ * @returns The resolved absolute path
219
+ *
220
+ * @example
221
+ * resolvePath("/user/name", "/items/0"); // "/user/name" (absolute)
222
+ * resolvePath("name", "/items/0"); // "/items/0/name" (relative)
223
+ * resolvePath("name", null); // "/name" (root scope)
224
+ */
225
+ export function resolvePath(path, basePath) {
226
+ if (isAbsolutePath(path)) {
227
+ return normalizePath(path);
228
+ }
229
+ if (basePath === null || basePath === '/') {
230
+ return normalizePath(path);
231
+ }
232
+ return joinPaths(basePath, path);
233
+ }
234
+ /**
235
+ * Joins two paths together.
236
+ *
237
+ * @param basePath - The base path
238
+ * @param relativePath - The relative path to join
239
+ * @returns The joined path
240
+ *
241
+ * @example
242
+ * joinPaths("/user", "name"); // "/user/name"
243
+ * joinPaths("/user", "/name"); // "/user/name"
244
+ * joinPaths("/user/", "/name/"); // "/user/name"
245
+ */
246
+ export function joinPaths(basePath, relativePath) {
247
+ const base = normalizePath(basePath);
248
+ const relative = relativePath.trim().replace(/^\/+/, '').replace(/\/+$/, '');
249
+ if (!relative) {
250
+ return base;
251
+ }
252
+ if (base === '/') {
253
+ return '/' + relative;
254
+ }
255
+ return base + '/' + relative;
256
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * pathUtils Tests
3
+ *
4
+ * Tests for path utility functions used in A2UI 0.9 data model operations.
5
+ */
6
+ export {};