@globaltypesystem/gts-ts 0.1.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.
- package/.eslintrc.json +16 -0
- package/.github/workflows/ci.yml +198 -0
- package/.gitmodules +3 -0
- package/.prettierrc +7 -0
- package/LICENSE +201 -0
- package/Makefile +64 -0
- package/README.md +298 -0
- package/dist/cast.d.ts +9 -0
- package/dist/cast.d.ts.map +1 -0
- package/dist/cast.js +153 -0
- package/dist/cast.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +318 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/compatibility.d.ts +11 -0
- package/dist/compatibility.d.ts.map +1 -0
- package/dist/compatibility.js +176 -0
- package/dist/compatibility.js.map +1 -0
- package/dist/extract.d.ts +13 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +194 -0
- package/dist/extract.js.map +1 -0
- package/dist/gts.d.ts +18 -0
- package/dist/gts.d.ts.map +1 -0
- package/dist/gts.js +472 -0
- package/dist/gts.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -0
- package/dist/query.d.ts +10 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +171 -0
- package/dist/query.js.map +1 -0
- package/dist/relationships.d.ts +7 -0
- package/dist/relationships.d.ts.map +1 -0
- package/dist/relationships.js +80 -0
- package/dist/relationships.js.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +132 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/server.d.ts +33 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +678 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/types.d.ts +61 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +3 -0
- package/dist/server/types.js.map +1 -0
- package/dist/store.d.ts +39 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +1026 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +111 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +29 -0
- package/dist/types.js.map +1 -0
- package/dist/x-gts-ref.d.ts +35 -0
- package/dist/x-gts-ref.d.ts.map +1 -0
- package/dist/x-gts-ref.js +304 -0
- package/dist/x-gts-ref.js.map +1 -0
- package/jest.config.js +13 -0
- package/package.json +54 -0
- package/src/cast.ts +179 -0
- package/src/cli/index.ts +315 -0
- package/src/compatibility.ts +201 -0
- package/src/extract.ts +213 -0
- package/src/gts.ts +550 -0
- package/src/index.ts +97 -0
- package/src/query.ts +191 -0
- package/src/relationships.ts +91 -0
- package/src/server/index.ts +112 -0
- package/src/server/server.ts +771 -0
- package/src/server/types.ts +74 -0
- package/src/store.ts +1178 -0
- package/src/types.ts +138 -0
- package/src/x-gts-ref.ts +349 -0
- package/tests/gts.test.ts +525 -0
- package/tsconfig.json +32 -0
package/src/gts.ts
ADDED
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import { v5 as uuidv5 } from 'uuid';
|
|
2
|
+
import {
|
|
3
|
+
GTS_PREFIX,
|
|
4
|
+
MAX_ID_LENGTH,
|
|
5
|
+
GtsID,
|
|
6
|
+
GtsIDSegment,
|
|
7
|
+
InvalidGtsIDError,
|
|
8
|
+
InvalidSegmentError,
|
|
9
|
+
ValidationResult,
|
|
10
|
+
ParseResult,
|
|
11
|
+
MatchResult,
|
|
12
|
+
UUIDResult,
|
|
13
|
+
} from './types';
|
|
14
|
+
|
|
15
|
+
const GTS_NAMESPACE = uuidv5('gts', uuidv5.URL);
|
|
16
|
+
|
|
17
|
+
const SEGMENT_TOKEN_REGEX = /^[a-z_][a-z0-9_]*$/;
|
|
18
|
+
|
|
19
|
+
export class Gts {
|
|
20
|
+
static parseGtsID(id: string): GtsID {
|
|
21
|
+
// If ID contains wildcard, validate as wildcard first
|
|
22
|
+
if (id.includes('*')) {
|
|
23
|
+
return this.validateWildcard(id);
|
|
24
|
+
}
|
|
25
|
+
return this.parseGtsIDInternal(id, false);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private static splitPreservingTilde(s: string): string[] {
|
|
29
|
+
const parts: string[] = [];
|
|
30
|
+
let current = '';
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < s.length; i++) {
|
|
33
|
+
if (s[i] === '~') {
|
|
34
|
+
// Add the segment with the tilde
|
|
35
|
+
parts.push(current + '~');
|
|
36
|
+
current = '';
|
|
37
|
+
} else {
|
|
38
|
+
current += s[i];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Add any remaining content (instance without trailing ~)
|
|
43
|
+
if (current) {
|
|
44
|
+
parts.push(current);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return parts.filter((p) => p !== '~'); // Remove any standalone tildes
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private static parseSegment(num: number, offset: number, segment: string): GtsIDSegment {
|
|
51
|
+
const seg: GtsIDSegment = {
|
|
52
|
+
num,
|
|
53
|
+
offset,
|
|
54
|
+
segment: segment.trim(),
|
|
55
|
+
vendor: '',
|
|
56
|
+
package: '',
|
|
57
|
+
namespace: '',
|
|
58
|
+
type: '',
|
|
59
|
+
verMajor: 0,
|
|
60
|
+
verMinor: undefined,
|
|
61
|
+
isType: false,
|
|
62
|
+
isWildcard: false,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
let workingSegment = seg.segment;
|
|
66
|
+
|
|
67
|
+
// Check for empty segment
|
|
68
|
+
if (!workingSegment || workingSegment === '~') {
|
|
69
|
+
throw new InvalidSegmentError(num, offset, segment, 'Empty segment');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const tildeCount = (workingSegment.match(/~/g) || []).length;
|
|
73
|
+
if (tildeCount > 0) {
|
|
74
|
+
if (tildeCount > 1) {
|
|
75
|
+
throw new InvalidSegmentError(num, offset, segment, "Too many '~' characters");
|
|
76
|
+
}
|
|
77
|
+
if (workingSegment.endsWith('~')) {
|
|
78
|
+
seg.isType = true;
|
|
79
|
+
workingSegment = workingSegment.slice(0, -1);
|
|
80
|
+
} else {
|
|
81
|
+
throw new InvalidSegmentError(num, offset, segment, " '~' must be at the end");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check for empty tokens (double dots)
|
|
86
|
+
if (workingSegment.includes('..')) {
|
|
87
|
+
throw new InvalidSegmentError(num, offset, segment, 'Empty token (double dots)');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const tokens = workingSegment.split('.');
|
|
91
|
+
|
|
92
|
+
// Check for empty tokens
|
|
93
|
+
for (const token of tokens) {
|
|
94
|
+
if (token === '') {
|
|
95
|
+
throw new InvalidSegmentError(num, offset, segment, 'Empty token');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (tokens.length > 6) {
|
|
100
|
+
throw new InvalidSegmentError(num, offset, segment, 'Too many tokens');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!workingSegment.endsWith('*')) {
|
|
104
|
+
if (tokens.length < 5) {
|
|
105
|
+
throw new InvalidSegmentError(num, offset, segment, 'Too few tokens');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (let t = 0; t < 4; t++) {
|
|
109
|
+
if (!SEGMENT_TOKEN_REGEX.test(tokens[t])) {
|
|
110
|
+
throw new InvalidSegmentError(num, offset, segment, 'Invalid segment token: ' + tokens[t]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (tokens.length > 0) {
|
|
116
|
+
if (tokens[0] === '*') {
|
|
117
|
+
seg.isWildcard = true;
|
|
118
|
+
return seg;
|
|
119
|
+
}
|
|
120
|
+
seg.vendor = tokens[0];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (tokens.length > 1) {
|
|
124
|
+
if (tokens[1] === '*') {
|
|
125
|
+
seg.isWildcard = true;
|
|
126
|
+
return seg;
|
|
127
|
+
}
|
|
128
|
+
seg.package = tokens[1];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (tokens.length > 2) {
|
|
132
|
+
if (tokens[2] === '*') {
|
|
133
|
+
seg.isWildcard = true;
|
|
134
|
+
return seg;
|
|
135
|
+
}
|
|
136
|
+
seg.namespace = tokens[2];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (tokens.length > 3) {
|
|
140
|
+
if (tokens[3] === '*') {
|
|
141
|
+
seg.isWildcard = true;
|
|
142
|
+
return seg;
|
|
143
|
+
}
|
|
144
|
+
seg.type = tokens[3];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (tokens.length > 4) {
|
|
148
|
+
if (tokens[4] === '*') {
|
|
149
|
+
seg.isWildcard = true;
|
|
150
|
+
return seg;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!tokens[4].startsWith('v')) {
|
|
154
|
+
throw new InvalidSegmentError(num, offset, segment, "Major version must start with 'v'");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const majorStr = tokens[4].substring(1);
|
|
158
|
+
const major = parseInt(majorStr, 10);
|
|
159
|
+
|
|
160
|
+
if (isNaN(major)) {
|
|
161
|
+
throw new InvalidSegmentError(num, offset, segment, 'Major version must be an integer');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (major < 0) {
|
|
165
|
+
throw new InvalidSegmentError(num, offset, segment, 'Major version must be >= 0');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (major.toString() !== majorStr) {
|
|
169
|
+
throw new InvalidSegmentError(num, offset, segment, 'Major version must be an integer');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
seg.verMajor = major;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (tokens.length > 5) {
|
|
176
|
+
if (tokens[5] === '*') {
|
|
177
|
+
seg.isWildcard = true;
|
|
178
|
+
return seg;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const minor = parseInt(tokens[5], 10);
|
|
182
|
+
|
|
183
|
+
if (isNaN(minor)) {
|
|
184
|
+
throw new InvalidSegmentError(num, offset, segment, 'Minor version must be an integer');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (minor < 0) {
|
|
188
|
+
throw new InvalidSegmentError(num, offset, segment, 'Minor version must be >= 0');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (minor.toString() !== tokens[5]) {
|
|
192
|
+
throw new InvalidSegmentError(num, offset, segment, 'Minor version must be an integer');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
seg.verMinor = minor;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return seg;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
static isValidGtsID(id: string): boolean {
|
|
202
|
+
if (!id.startsWith(GTS_PREFIX)) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
this.parseGtsID(id);
|
|
207
|
+
return true;
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
static validateGtsID(id: string): ValidationResult {
|
|
214
|
+
const isWildcard = id.includes('*');
|
|
215
|
+
try {
|
|
216
|
+
if (isWildcard) {
|
|
217
|
+
// For wildcard patterns, use validateWildcard
|
|
218
|
+
this.validateWildcard(id);
|
|
219
|
+
} else {
|
|
220
|
+
this.parseGtsID(id);
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
id,
|
|
224
|
+
ok: true,
|
|
225
|
+
valid: true,
|
|
226
|
+
error: '',
|
|
227
|
+
is_wildcard: isWildcard,
|
|
228
|
+
};
|
|
229
|
+
} catch (error) {
|
|
230
|
+
return {
|
|
231
|
+
id,
|
|
232
|
+
ok: false,
|
|
233
|
+
valid: false,
|
|
234
|
+
error: error instanceof Error ? error.message : String(error),
|
|
235
|
+
is_wildcard: isWildcard,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
static parseID(id: string): ParseResult {
|
|
241
|
+
try {
|
|
242
|
+
const gtsId = this.parseGtsID(id);
|
|
243
|
+
return {
|
|
244
|
+
ok: true,
|
|
245
|
+
segments: gtsId.segments,
|
|
246
|
+
};
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return {
|
|
249
|
+
ok: false,
|
|
250
|
+
segments: [],
|
|
251
|
+
error: error instanceof Error ? error.message : String(error),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
static isType(id: string): boolean {
|
|
257
|
+
return id.endsWith('~');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
static toUUID(id: string): string {
|
|
261
|
+
return uuidv5(id, GTS_NAMESPACE);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
static idToUUID(id: string): UUIDResult {
|
|
265
|
+
try {
|
|
266
|
+
this.parseGtsID(id);
|
|
267
|
+
return {
|
|
268
|
+
id,
|
|
269
|
+
uuid: this.toUUID(id),
|
|
270
|
+
};
|
|
271
|
+
} catch (error) {
|
|
272
|
+
return {
|
|
273
|
+
id,
|
|
274
|
+
uuid: '',
|
|
275
|
+
error: error instanceof Error ? error.message : String(error),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
static matchIDPattern(candidate: string, pattern: string): MatchResult {
|
|
281
|
+
try {
|
|
282
|
+
// Validate and parse candidate
|
|
283
|
+
// If candidate contains '*', validate it as a wildcard pattern first
|
|
284
|
+
// This catches malformed wildcards like 'a*' (wildcard not on token boundary)
|
|
285
|
+
let candidateId: GtsID;
|
|
286
|
+
try {
|
|
287
|
+
if (candidate.includes('*')) {
|
|
288
|
+
// Validate candidate as a wildcard pattern first
|
|
289
|
+
this.validateWildcard(candidate);
|
|
290
|
+
}
|
|
291
|
+
candidateId = this.parseGtsID(candidate);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
return {
|
|
294
|
+
match: false,
|
|
295
|
+
pattern,
|
|
296
|
+
candidate,
|
|
297
|
+
error: error instanceof Error ? error.message : String(error),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Validate and parse pattern (allow wildcards)
|
|
302
|
+
let patternId: GtsID;
|
|
303
|
+
try {
|
|
304
|
+
patternId = this.validateWildcard(pattern);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
return {
|
|
307
|
+
match: false,
|
|
308
|
+
pattern,
|
|
309
|
+
candidate,
|
|
310
|
+
error: error instanceof Error ? error.message : String(error),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Perform matching
|
|
315
|
+
const match = this.wildcardMatch(candidateId, patternId);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
match,
|
|
319
|
+
pattern,
|
|
320
|
+
candidate,
|
|
321
|
+
};
|
|
322
|
+
} catch (error) {
|
|
323
|
+
return {
|
|
324
|
+
match: false,
|
|
325
|
+
pattern,
|
|
326
|
+
candidate,
|
|
327
|
+
error: error instanceof Error ? error.message : String(error),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private static validateWildcard(pattern: string): GtsID {
|
|
333
|
+
const p = pattern.trim();
|
|
334
|
+
|
|
335
|
+
// Must start with gts.
|
|
336
|
+
if (!p.startsWith(GTS_PREFIX)) {
|
|
337
|
+
throw new InvalidGtsIDError(pattern, `Does not start with '${GTS_PREFIX}'`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Count wildcards
|
|
341
|
+
const wildcardCount = (p.match(/\*/g) || []).length;
|
|
342
|
+
if (wildcardCount > 1) {
|
|
343
|
+
throw new InvalidGtsIDError(pattern, "The wildcard '*' token is allowed only once");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// If wildcard exists, validate its position
|
|
347
|
+
if (wildcardCount === 1) {
|
|
348
|
+
// Wildcard must be at a token boundary at the end (either .* or ~*)
|
|
349
|
+
// Pattern like "gts.a.b.c.d.v1~a.*" is valid (wildcard at end after .)
|
|
350
|
+
// Pattern like "gts.a.b.c.d.v1~a.*~" is invalid (wildcard not at end of pattern)
|
|
351
|
+
// Pattern like "gts.a.b.c.d.v1~a*" is invalid (wildcard not at token boundary)
|
|
352
|
+
// Pattern like "gts.a.b.c.*.v1~a.*" is invalid (wildcard in middle of segment)
|
|
353
|
+
|
|
354
|
+
const wildcardIndex = p.indexOf('*');
|
|
355
|
+
|
|
356
|
+
// Check if wildcard is at the very end
|
|
357
|
+
if (wildcardIndex !== p.length - 1) {
|
|
358
|
+
throw new InvalidGtsIDError(pattern, "The wildcard '*' token is allowed only at the end of the pattern");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Check if wildcard is preceded by . or ~ (token boundary)
|
|
362
|
+
if (wildcardIndex > 0 && p[wildcardIndex - 1] !== '.' && p[wildcardIndex - 1] !== '~') {
|
|
363
|
+
throw new InvalidGtsIDError(pattern, "The wildcard '*' must be preceded by '.' or '~' (token boundary)");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Check that there's no wildcard in the middle of a segment (before a ~)
|
|
367
|
+
// Split by ~ to get segments, check if any segment except the last has a wildcard
|
|
368
|
+
const segments = p.split('~');
|
|
369
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
370
|
+
if (segments[i].includes('*')) {
|
|
371
|
+
throw new InvalidGtsIDError(pattern, "The wildcard '*' token cannot appear in the middle of a chained ID");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Parse the pattern - parseGtsID will handle the segment validation
|
|
377
|
+
return this.parseGtsIDInternal(p, true);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Internal parse method that can be called with wildcard mode
|
|
381
|
+
private static parseGtsIDInternal(id: string, allowWildcard: boolean = false): GtsID {
|
|
382
|
+
const raw = id.trim();
|
|
383
|
+
|
|
384
|
+
if (raw !== raw.toLowerCase()) {
|
|
385
|
+
throw new InvalidGtsIDError(id, 'Must be lower case');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (raw.includes('-')) {
|
|
389
|
+
throw new InvalidGtsIDError(id, "Must not contain '-'");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (!raw.startsWith(GTS_PREFIX)) {
|
|
393
|
+
throw new InvalidGtsIDError(id, `Does not start with '${GTS_PREFIX}'`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (raw.length > MAX_ID_LENGTH) {
|
|
397
|
+
throw new InvalidGtsIDError(id, 'Too long');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Additional validation
|
|
401
|
+
if (raw.includes('..')) {
|
|
402
|
+
throw new InvalidGtsIDError(id, 'Double dots not allowed');
|
|
403
|
+
}
|
|
404
|
+
if (raw.endsWith('.') && !raw.endsWith('.*')) {
|
|
405
|
+
throw new InvalidGtsIDError(id, 'Cannot end with a dot');
|
|
406
|
+
}
|
|
407
|
+
if (raw.includes('~~')) {
|
|
408
|
+
throw new InvalidGtsIDError(id, 'Double tildes not allowed');
|
|
409
|
+
}
|
|
410
|
+
if (raw === GTS_PREFIX || raw === GTS_PREFIX + '~') {
|
|
411
|
+
throw new InvalidGtsIDError(id, 'ID cannot be just the prefix');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const gtsId: GtsID = {
|
|
415
|
+
id: raw,
|
|
416
|
+
segments: [],
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const remainder = raw.substring(GTS_PREFIX.length);
|
|
420
|
+
const parts = this.splitPreservingTilde(remainder);
|
|
421
|
+
|
|
422
|
+
let offset = GTS_PREFIX.length;
|
|
423
|
+
for (let i = 0; i < parts.length; i++) {
|
|
424
|
+
const part = parts[i];
|
|
425
|
+
if (part === '') {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const segment = this.parseSegment(i + 1, offset, part);
|
|
430
|
+
gtsId.segments.push(segment);
|
|
431
|
+
offset += part.length;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Ensure we have at least one segment
|
|
435
|
+
if (gtsId.segments.length === 0) {
|
|
436
|
+
throw new InvalidGtsIDError(id, 'No valid segments found');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// v0.7: Single-segment instance IDs are prohibited (skip for wildcard patterns)
|
|
440
|
+
if (!allowWildcard && !raw.includes('*')) {
|
|
441
|
+
const lastSegment = gtsId.segments[gtsId.segments.length - 1];
|
|
442
|
+
if (!lastSegment.isType && gtsId.segments.length === 1) {
|
|
443
|
+
throw new InvalidGtsIDError(
|
|
444
|
+
id,
|
|
445
|
+
'Single-segment instance IDs are prohibited. Instance IDs must be chained with a type segment (e.g., gts.vendor.pkg.ns.type.v1~instance.segment.v1)'
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return gtsId;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private static wildcardMatch(candidate: GtsID, pattern: GtsID): boolean {
|
|
454
|
+
if (!candidate || !pattern) {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// If no wildcard in pattern, perform exact match with version flexibility
|
|
459
|
+
if (!pattern.id.includes('*')) {
|
|
460
|
+
return this.matchSegments(pattern.segments, candidate.segments);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Wildcard case
|
|
464
|
+
if ((pattern.id.match(/\*/g) || []).length > 1 || !pattern.id.endsWith('*')) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Use segment matching for wildcard patterns too
|
|
469
|
+
return this.matchSegments(pattern.segments, candidate.segments);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private static matchSegments(patternSegs: GtsIDSegment[], candidateSegs: GtsIDSegment[]): boolean {
|
|
473
|
+
// If pattern is longer than candidate, no match
|
|
474
|
+
if (patternSegs.length > candidateSegs.length) {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
for (let i = 0; i < patternSegs.length; i++) {
|
|
479
|
+
const pSeg = patternSegs[i];
|
|
480
|
+
const cSeg = candidateSegs[i];
|
|
481
|
+
|
|
482
|
+
// If pattern segment is a wildcard, check non-wildcard fields first
|
|
483
|
+
if (pSeg.isWildcard) {
|
|
484
|
+
// Check the fields that are set (non-empty) in the wildcard pattern
|
|
485
|
+
if (pSeg.vendor && pSeg.vendor !== cSeg.vendor) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
if (pSeg.package && pSeg.package !== cSeg.package) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
if (pSeg.namespace && pSeg.namespace !== cSeg.namespace) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
if (pSeg.type && pSeg.type !== cSeg.type) {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
// Check version fields if they are set in the pattern
|
|
498
|
+
if (pSeg.verMajor !== 0 && pSeg.verMajor !== cSeg.verMajor) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
if (pSeg.verMinor !== undefined && (cSeg.verMinor === undefined || pSeg.verMinor !== cSeg.verMinor)) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
// Check is_type flag if set
|
|
505
|
+
if (pSeg.isType && pSeg.isType !== cSeg.isType) {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
// Wildcard matches - accept anything after this point
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Non-wildcard segment - all fields must match
|
|
513
|
+
if (pSeg.vendor !== cSeg.vendor) {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
if (pSeg.package !== cSeg.package) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
if (pSeg.namespace !== cSeg.namespace) {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
if (pSeg.type !== cSeg.type) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Check version matching
|
|
527
|
+
// Major version must match
|
|
528
|
+
if (pSeg.verMajor !== cSeg.verMajor) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Minor version: if pattern has no minor version, accept any minor in candidate
|
|
533
|
+
// If pattern has minor version, it must match exactly
|
|
534
|
+
if (pSeg.verMinor !== undefined) {
|
|
535
|
+
if (cSeg.verMinor === undefined || pSeg.verMinor !== cSeg.verMinor) {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// else: pattern has no minor version, so any minor version in candidate is OK
|
|
540
|
+
|
|
541
|
+
// Check is_type flag matches
|
|
542
|
+
if (pSeg.isType !== cSeg.isType) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// If we've matched all pattern segments, it's a match
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export * from './types';
|
|
2
|
+
export { Gts } from './gts';
|
|
3
|
+
export { GtsExtractor } from './extract';
|
|
4
|
+
export { GtsStore, createJsonEntity } from './store';
|
|
5
|
+
export { GtsRelationships } from './relationships';
|
|
6
|
+
export { GtsCompatibility } from './compatibility';
|
|
7
|
+
export { GtsCast } from './cast';
|
|
8
|
+
export { GtsQuery } from './query';
|
|
9
|
+
|
|
10
|
+
import { Gts } from './gts';
|
|
11
|
+
import { GtsExtractor } from './extract';
|
|
12
|
+
import { GtsStore, createJsonEntity } from './store';
|
|
13
|
+
import { GtsRelationships } from './relationships';
|
|
14
|
+
import { GtsCompatibility } from './compatibility';
|
|
15
|
+
import { GtsCast } from './cast';
|
|
16
|
+
import { GtsQuery } from './query';
|
|
17
|
+
import {
|
|
18
|
+
ValidationResult,
|
|
19
|
+
ParseResult,
|
|
20
|
+
MatchResult,
|
|
21
|
+
UUIDResult,
|
|
22
|
+
ExtractResult,
|
|
23
|
+
AttributeResult,
|
|
24
|
+
QueryResult,
|
|
25
|
+
RelationshipResult,
|
|
26
|
+
CompatibilityResult,
|
|
27
|
+
CastResult,
|
|
28
|
+
GtsConfig,
|
|
29
|
+
} from './types';
|
|
30
|
+
|
|
31
|
+
export const isValidGtsID = (id: string): boolean => Gts.isValidGtsID(id);
|
|
32
|
+
export const validateGtsID = (id: string): ValidationResult => Gts.validateGtsID(id);
|
|
33
|
+
export const parseGtsID = (id: string): ParseResult => Gts.parseID(id);
|
|
34
|
+
export const matchIDPattern = (candidate: string, pattern: string): MatchResult =>
|
|
35
|
+
Gts.matchIDPattern(candidate, pattern);
|
|
36
|
+
export const idToUUID = (id: string): UUIDResult => Gts.idToUUID(id);
|
|
37
|
+
export const extractID = (content: any, schemaContent?: any): ExtractResult =>
|
|
38
|
+
GtsExtractor.extractID(content, schemaContent);
|
|
39
|
+
|
|
40
|
+
export class GTS {
|
|
41
|
+
private store: GtsStore;
|
|
42
|
+
|
|
43
|
+
constructor(config?: Partial<GtsConfig>) {
|
|
44
|
+
this.store = new GtsStore(config);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
register(content: any): void {
|
|
48
|
+
const entity = createJsonEntity(content);
|
|
49
|
+
this.store.register(entity);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get(id: string): any {
|
|
53
|
+
const entity = this.store.get(id);
|
|
54
|
+
return entity?.content;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
validateInstance(id: string): ValidationResult {
|
|
58
|
+
return this.store.validateInstance(id);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getAttribute(path: string): AttributeResult {
|
|
62
|
+
// Parse the combined path format: gts_id@attr_path
|
|
63
|
+
const atIndex = path.indexOf('@');
|
|
64
|
+
if (atIndex === -1) {
|
|
65
|
+
return {
|
|
66
|
+
path,
|
|
67
|
+
resolved: false,
|
|
68
|
+
error: 'Invalid attribute path: missing @',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const gtsId = path.substring(0, atIndex);
|
|
72
|
+
const attrPath = path.substring(atIndex + 1);
|
|
73
|
+
return this.store.getAttribute(gtsId, attrPath);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
query(expression: string, limit?: number): QueryResult {
|
|
77
|
+
return GtsQuery.query(this.store, expression, limit);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
resolveRelationships(id: string): RelationshipResult {
|
|
81
|
+
return GtsRelationships.resolveRelationships(this.store, id);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
checkCompatibility(
|
|
85
|
+
oldId: string,
|
|
86
|
+
newId: string,
|
|
87
|
+
mode: 'backward' | 'forward' | 'full' = 'full'
|
|
88
|
+
): CompatibilityResult {
|
|
89
|
+
return GtsCompatibility.checkCompatibility(this.store, oldId, newId, mode);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
castInstance(fromId: string, toSchemaId: string): CastResult {
|
|
93
|
+
return GtsCast.castInstance(this.store, fromId, toSchemaId);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default GTS;
|