@arkadia/ai-data-format 1.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.
- package/dist/config.d.ts +75 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +28 -0
- package/dist/config.js.map +1 -0
- package/dist/core/Decoder.d.ts +88 -0
- package/dist/core/Decoder.d.ts.map +1 -0
- package/dist/core/Decoder.js +968 -0
- package/dist/core/Decoder.js.map +1 -0
- package/dist/core/Encoder.d.ts +26 -0
- package/dist/core/Encoder.d.ts.map +1 -0
- package/dist/core/Encoder.js +368 -0
- package/dist/core/Encoder.js.map +1 -0
- package/dist/core/Parser.d.ts +6 -0
- package/dist/core/Parser.d.ts.map +1 -0
- package/dist/core/Parser.js +132 -0
- package/dist/core/Parser.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/models/Meta.d.ts +48 -0
- package/dist/models/Meta.d.ts.map +1 -0
- package/dist/models/Meta.js +113 -0
- package/dist/models/Meta.js.map +1 -0
- package/dist/models/Node.d.ts +42 -0
- package/dist/models/Node.d.ts.map +1 -0
- package/dist/models/Node.js +179 -0
- package/dist/models/Node.js.map +1 -0
- package/dist/models/Schema.d.ts +55 -0
- package/dist/models/Schema.d.ts.map +1 -0
- package/dist/models/Schema.js +175 -0
- package/dist/models/Schema.js.map +1 -0
- package/package.json +32 -0
- package/src/config.ts +102 -0
- package/src/core/Decoder.ts +1074 -0
- package/src/core/Encoder.ts +443 -0
- package/src/core/Parser.ts +150 -0
- package/src/index.ts +46 -0
- package/src/models/Meta.ts +135 -0
- package/src/models/Node.ts +212 -0
- package/src/models/Schema.ts +222 -0
- package/tests/00.meta.test.ts +31 -0
- package/tests/00.node.test.ts +54 -0
- package/tests/00.primitive.test.ts +108 -0
- package/tests/00.schema.test.ts +41 -0
- package/tests/01.schema.test.ts +70 -0
- package/tests/02.data.test.ts +89 -0
- package/tests/03.errors.test.ts +71 -0
- package/tests/04.list.test.ts +225 -0
- package/tests/05.record.test.ts +82 -0
- package/tests/06.meta.test.ts +506 -0
- package/tests/utils.ts +69 -0
- package/tsconfig.json +46 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { decode, encode, Node, Schema, SchemaKind } from '../src/index';
|
|
3
|
+
import { assertRoundtrip } from './utils';
|
|
4
|
+
|
|
5
|
+
describe('AI Data Metadata (Meta)', () => {
|
|
6
|
+
|
|
7
|
+
// ==================================================================================
|
|
8
|
+
// 2. SCHEMA DEFINITION & TYPING
|
|
9
|
+
// ==================================================================================
|
|
10
|
+
|
|
11
|
+
it('should handle comments', () => {
|
|
12
|
+
/** Validates that comments /.../ are ignored or handled. */
|
|
13
|
+
const text = '@User<id:int /*primary key*/, name:string> @User(5, "Bob")';
|
|
14
|
+
// Note: int normalizes to number in TS
|
|
15
|
+
const expected = '@User</*primary key*/ id:number,name:string>(5,"Bob")';
|
|
16
|
+
|
|
17
|
+
const res = decode(text, { debug: false });
|
|
18
|
+
expect(res.errors).toHaveLength(0);
|
|
19
|
+
|
|
20
|
+
// If the parser attaches schema comments to fields correctly, good.
|
|
21
|
+
// Here we just ensure data parsing works despite comments.
|
|
22
|
+
expect(res.node.fields["id"].value).toBe(5);
|
|
23
|
+
|
|
24
|
+
assertRoundtrip(text, expected, false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should handle meta header', () => {
|
|
28
|
+
const aidText = `
|
|
29
|
+
$a0=5
|
|
30
|
+
<
|
|
31
|
+
/* c1 */
|
|
32
|
+
/ $a1 /* c0 *//
|
|
33
|
+
/* c2 */ $a2=2 /* c3 */ $a3=3 a:number
|
|
34
|
+
>
|
|
35
|
+
($a6 /*a*/ 3)
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const expected = "<//*c0*/ $a0=5 $a1=true/ /*c1*/ /*c2*/ /*c3*/ $a2=2 $a3=3 a:number>(/*a*/ $a6=true 3)";
|
|
39
|
+
assertRoundtrip(aidText, expected, false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should handle mixed meta', () => {
|
|
43
|
+
const aidText = `
|
|
44
|
+
$attr=5
|
|
45
|
+
<
|
|
46
|
+
/* comm2 */
|
|
47
|
+
/ $schema1 /
|
|
48
|
+
/* comm1 */
|
|
49
|
+
[a:int]
|
|
50
|
+
>
|
|
51
|
+
$attr=3
|
|
52
|
+
[
|
|
53
|
+
/ /*meta for list*/ $attr=4 /
|
|
54
|
+
/*item1*/ $attr5 (3 $attr6),
|
|
55
|
+
/*item2*/ {a:5},
|
|
56
|
+
]
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
// Note: int -> number
|
|
60
|
+
const expected = "<[//*comm2*/ /*comm1*/ $attr=5 $schema1=true/ a:number]>[//*meta for list*/ $attr=4/ (//*item1*/ $attr5=true/ $attr6=true 3),(//*item2*// 5)]";
|
|
61
|
+
assertRoundtrip(aidText, expected, false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ==============================================================================
|
|
65
|
+
// 1. SCHEMA DEFINITION META (Types defined in < ... >)
|
|
66
|
+
// ==============================================================================
|
|
67
|
+
|
|
68
|
+
it('should handle list schema with meta', () => {
|
|
69
|
+
/**
|
|
70
|
+
* Verifies that an empty schema definition < ... > correctly parses:
|
|
71
|
+
* 1. Comments (including nested ones).
|
|
72
|
+
* 2. Attributes ($key=val).
|
|
73
|
+
* 3. Tags (#tag).
|
|
74
|
+
*/
|
|
75
|
+
const aidText = `
|
|
76
|
+
/* 0 */
|
|
77
|
+
<
|
|
78
|
+
/* commentm0 */ /* com1 /*com1.2*/ */
|
|
79
|
+
/ $listAttr="GlobalList" $b=4 #tag /
|
|
80
|
+
/* comment4 */
|
|
81
|
+
id:number
|
|
82
|
+
>
|
|
83
|
+
/* a */
|
|
84
|
+
#tag1 $val=3
|
|
85
|
+
[
|
|
86
|
+
1,
|
|
87
|
+
2,
|
|
88
|
+
3
|
|
89
|
+
]
|
|
90
|
+
/* b */
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
const result = decode(aidText, { debug: false });
|
|
94
|
+
const node = result.node;
|
|
95
|
+
const errors = result.errors;
|
|
96
|
+
|
|
97
|
+
// 1. Assert no syntax errors
|
|
98
|
+
expect(errors).toHaveLength(0);
|
|
99
|
+
|
|
100
|
+
// 2. Check Schema Basics
|
|
101
|
+
const schema = node.schema;
|
|
102
|
+
expect(schema).not.toBeNull();
|
|
103
|
+
expect(schema.kind).toBe(SchemaKind.LIST); // Default kind for <...>
|
|
104
|
+
|
|
105
|
+
// 3. Verify Attributes ($key=val)
|
|
106
|
+
// Parser casts to number if possible
|
|
107
|
+
expect(schema.attr["listAttr"]).toBe("GlobalList");
|
|
108
|
+
expect(schema.attr["b"]).toBe(4);
|
|
109
|
+
|
|
110
|
+
// 4. Verify Tags (#tag)
|
|
111
|
+
expect(schema.tags).toContain("tag");
|
|
112
|
+
expect(schema.tags).toHaveLength(1);
|
|
113
|
+
|
|
114
|
+
// 5. Verify Comments
|
|
115
|
+
// We expect multiple comments to be collected
|
|
116
|
+
expect(schema.comments.length).toBeGreaterThan(0);
|
|
117
|
+
expect(schema.comments.some(c => c.includes("0"))).toBe(true);
|
|
118
|
+
|
|
119
|
+
const expected = '<[//*0*/ $listAttr="GlobalList" $b=4 #tag/ number]>[//*a*/ /*b*/ $val=3 #tag1/ 1,2,3]';
|
|
120
|
+
assertRoundtrip(aidText, expected, false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should encode manual schema with meta', () => {
|
|
124
|
+
/**
|
|
125
|
+
* Verifies that a Schema with meta/comments is encoded correctly.
|
|
126
|
+
* Expected format: < / $attr=val #tag / any >
|
|
127
|
+
*/
|
|
128
|
+
|
|
129
|
+
// 1. Prepare Schema manually
|
|
130
|
+
const schema = new Schema(SchemaKind.RECORD);
|
|
131
|
+
schema.comments = ["comment1", "comment2"];
|
|
132
|
+
schema.attr = { "key": "value", "count": 10 };
|
|
133
|
+
schema.tags = ["myTag"];
|
|
134
|
+
|
|
135
|
+
// Create a node using this schema
|
|
136
|
+
const node = new Node(schema, { value: null });
|
|
137
|
+
const expected = '<//*comment1*/ /*comment2*/ $key="value" $count=10 #myTag/ any>(null)';
|
|
138
|
+
|
|
139
|
+
assertRoundtrip(node, expected, false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should round trip schema encode decode', () => {
|
|
143
|
+
/**
|
|
144
|
+
* Verifies that a Schema with meta/comments can be encoded to text
|
|
145
|
+
* and then decoded back, preserving all metadata (Round-Trip).
|
|
146
|
+
*/
|
|
147
|
+
|
|
148
|
+
// 1. Prepare Schema manually
|
|
149
|
+
const originalSchema = new Schema(SchemaKind.RECORD);
|
|
150
|
+
originalSchema.comments = ["comment1", "comment2"];
|
|
151
|
+
originalSchema.attr = { "key": "value", "count": 10, "isActive": true };
|
|
152
|
+
originalSchema.tags = ["myTag", "urgent"];
|
|
153
|
+
|
|
154
|
+
// Create a node using this schema
|
|
155
|
+
const originalNode = new Node(originalSchema, { value: null });
|
|
156
|
+
|
|
157
|
+
// 2. Encode to String
|
|
158
|
+
// Important: We must enable include_comments to verify them after decoding
|
|
159
|
+
const encodedText = encode(originalNode, {
|
|
160
|
+
includeComments: true,
|
|
161
|
+
compact: true, // Test compact mode (one line)
|
|
162
|
+
colorize: false
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// 3. Decode back to Node
|
|
166
|
+
const result = decode(encodedText, { debug: false });
|
|
167
|
+
const decodedNode = result.node;
|
|
168
|
+
const errors = result.errors;
|
|
169
|
+
|
|
170
|
+
// 4. Verify No Errors
|
|
171
|
+
expect(errors).toHaveLength(0);
|
|
172
|
+
expect(decodedNode).not.toBeNull();
|
|
173
|
+
|
|
174
|
+
// 5. Verify Schema Integrity
|
|
175
|
+
const decodedSchema = decodedNode.schema;
|
|
176
|
+
expect(decodedSchema).not.toBeNull();
|
|
177
|
+
expect(decodedSchema.kind).toBe(SchemaKind.RECORD);
|
|
178
|
+
|
|
179
|
+
// 6. Verify Meta Data (Attributes)
|
|
180
|
+
expect(decodedSchema.attr["key"]).toBe("value");
|
|
181
|
+
expect(decodedSchema.attr["count"]).toBe(10);
|
|
182
|
+
expect(decodedSchema.attr["isActive"]).toBe(true);
|
|
183
|
+
|
|
184
|
+
// 7. Verify Tags
|
|
185
|
+
expect(decodedSchema.tags).toContain("myTag");
|
|
186
|
+
expect(decodedSchema.tags).toContain("urgent");
|
|
187
|
+
expect(decodedSchema.tags).toHaveLength(2);
|
|
188
|
+
|
|
189
|
+
// 8. Verify Comments
|
|
190
|
+
expect(decodedSchema.comments).toHaveLength(2);
|
|
191
|
+
expect(decodedSchema.comments).toContain("comment1");
|
|
192
|
+
expect(decodedSchema.comments).toContain("comment2");
|
|
193
|
+
|
|
194
|
+
// Additional checks for configuration flags
|
|
195
|
+
const decodedTextClean = encode(decodedNode, {
|
|
196
|
+
compact: true,
|
|
197
|
+
includeMeta: false,
|
|
198
|
+
includeComments: false
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Assertions for No Meta
|
|
202
|
+
expect(decodedTextClean).not.toContain("$key");
|
|
203
|
+
expect(decodedTextClean).not.toContain("#myTag");
|
|
204
|
+
expect(decodedTextClean).not.toContain("/"); // No meta block
|
|
205
|
+
// Empty schema without meta encodes to <any> or similar
|
|
206
|
+
expect(decodedTextClean).toContain("<any>");
|
|
207
|
+
|
|
208
|
+
// B. Encode WITH Meta (includeMeta=true) but NO Type (includeType=false)
|
|
209
|
+
const decodedTextWithMeta = encode(decodedNode, {
|
|
210
|
+
compact: true,
|
|
211
|
+
includeType: false,
|
|
212
|
+
includeMeta: true,
|
|
213
|
+
includeComments: false
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Assertions for With Meta
|
|
217
|
+
expect(decodedTextWithMeta).toContain("$key=");
|
|
218
|
+
expect(decodedTextWithMeta).toContain("#myTag");
|
|
219
|
+
expect(decodedTextWithMeta).toContain("$count=10");
|
|
220
|
+
expect(decodedTextWithMeta).toContain("/");
|
|
221
|
+
|
|
222
|
+
const expected = '<//*comment1*/ /*comment2*/ $key="value" $count=10 $isActive=true #myTag #urgent/ any>(null)';
|
|
223
|
+
assertRoundtrip(originalNode, expected, false);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should handle meta schema list vs element', () => {
|
|
227
|
+
/**
|
|
228
|
+
* Tests nested metadata within a type definition:
|
|
229
|
+
* Outer: / $listAttr="GlobalList" / -> Applies to the entire List
|
|
230
|
+
* Inner: / $elemAttr="InnerRecord" / -> Applies to the Element (Record) inside
|
|
231
|
+
*/
|
|
232
|
+
const aidText = `
|
|
233
|
+
<
|
|
234
|
+
/* comm-header-0 */ /* comm-header-1 /* comm-header-1.1*/ */
|
|
235
|
+
/ $listAttr="GlobalList" $b=4 /*com-in*/ /
|
|
236
|
+
/* comm-after-header-0 */
|
|
237
|
+
[
|
|
238
|
+
/ $elemAttr="InnerRecord" #elem0 /* comm-inside-header-0 */ /
|
|
239
|
+
/* comm-inside-field-0 */ #elem1 id: int
|
|
240
|
+
]
|
|
241
|
+
>
|
|
242
|
+
[ /* comm-data-v1 */ (1) /* comm-data-v2 */ ]
|
|
243
|
+
`;
|
|
244
|
+
|
|
245
|
+
const results = decode(aidText, { debug: false });
|
|
246
|
+
const node = results.node;
|
|
247
|
+
const errors = results.errors;
|
|
248
|
+
|
|
249
|
+
expect(errors).toHaveLength(0);
|
|
250
|
+
|
|
251
|
+
// 1. Check List Meta (Outer)
|
|
252
|
+
expect(node.isList).toBe(true);
|
|
253
|
+
// Access attributes via .attr (Mixin)
|
|
254
|
+
expect(node.schema.attr["listAttr"]).toBe("GlobalList");
|
|
255
|
+
expect(node.schema.attr["elemAttr"]).toBe("InnerRecord");
|
|
256
|
+
|
|
257
|
+
expect(node.schema.attr["b"]).toBe(4);
|
|
258
|
+
|
|
259
|
+
// 2. Check Element Meta (Inner Record)
|
|
260
|
+
const elemSchema = node.schema.element!;
|
|
261
|
+
expect(elemSchema.kind).toBe(SchemaKind.RECORD);
|
|
262
|
+
expect(elemSchema.attr).toStrictEqual({});
|
|
263
|
+
|
|
264
|
+
// Check if element meta propagated to actual data elements (depends on implementation)
|
|
265
|
+
// Usually schema meta is on schema, data meta is on data node.
|
|
266
|
+
// Here we check the schema attached to the element node.
|
|
267
|
+
expect(node.elements[0].schema.attr).toStrictEqual({});
|
|
268
|
+
|
|
269
|
+
const expected = '<[//*com-in*/ /*comm-header-0*/ /*comm-header-1 /* comm-header-1.1*/*/ /*comm-after-header-0*/ /*comm-inside-header-0*/ $listAttr="GlobalList" $b=4 $elemAttr="InnerRecord" #elem0/ /*comm-inside-field-0*/ #elem1 id:number]>[(//*comm-data-v1*/ /*comm-data-v2*// 1)]';
|
|
270
|
+
assertRoundtrip(aidText, expected, false);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should handle meta schema before fields', () => {
|
|
274
|
+
const aidText = `
|
|
275
|
+
<
|
|
276
|
+
/* header-com-0 */
|
|
277
|
+
/ #tag_header /
|
|
278
|
+
/* comm-data-v1 */ #tag1 v1: number /* comm-data-v2 */ #tag2,
|
|
279
|
+
/* comm-data-v3 */ #tag3 v2: number /* comm-data-v3 */ #tag4
|
|
280
|
+
>
|
|
281
|
+
[
|
|
282
|
+
/ #tag_list /
|
|
283
|
+
/* comm-data-v1 */ #tag1 1 /* comm-data-v2 */ #tag2
|
|
284
|
+
/* comm-data-v3 */ #tag3 2 /* comm-data-v3 */ #tag4
|
|
285
|
+
]
|
|
286
|
+
`;
|
|
287
|
+
|
|
288
|
+
const results = decode(aidText, { debug: false });
|
|
289
|
+
const node = results.node;
|
|
290
|
+
expect(results.errors).toHaveLength(0);
|
|
291
|
+
|
|
292
|
+
const expected = '<[/#tag_header/ number]>[/#tag_list/ /*comm-data-v1*/ #tag1 1,/*comm-data-v2*/ /*comm-data-v3*/ /*comm-data-v3*/ #tag2 #tag3 #tag4 2]';
|
|
293
|
+
assertRoundtrip(aidText, expected, false);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should warn on meta schema with implicit values', () => {
|
|
297
|
+
/**
|
|
298
|
+
* Tests handling of malformed meta blocks.
|
|
299
|
+
* In the input: / listAttr="GlobalList" / is missing '$' prefix for attribute.
|
|
300
|
+
*/
|
|
301
|
+
const aidText = `
|
|
302
|
+
<
|
|
303
|
+
/ listAttr="GlobalList" /
|
|
304
|
+
[
|
|
305
|
+
/* Missing $ prefix */
|
|
306
|
+
/ $elemAttr="InnerRecord" /* fixed input */ /
|
|
307
|
+
/* comments2 */ id: int
|
|
308
|
+
]
|
|
309
|
+
>
|
|
310
|
+
[ (1) ]
|
|
311
|
+
`;
|
|
312
|
+
|
|
313
|
+
const results = decode(aidText, { debug: false });
|
|
314
|
+
const node = results.node;
|
|
315
|
+
const warnings = results.warnings;
|
|
316
|
+
|
|
317
|
+
// If the input was fixed above, errors should be 0.
|
|
318
|
+
// Because we have: listAttr="GlobalList" (implicit), it should be a warning.
|
|
319
|
+
expect(warnings.length).toBeGreaterThan(0);
|
|
320
|
+
expect(warnings[0].message).toContain("Implicit attribute 'listAttr'");
|
|
321
|
+
|
|
322
|
+
// 1. Check List Meta (Outer)
|
|
323
|
+
expect(node.isList).toBe(true);
|
|
324
|
+
expect(node.schema.attr["listAttr"]).toBe("GlobalList");
|
|
325
|
+
|
|
326
|
+
// 2. Check Element Meta (Inner Record)
|
|
327
|
+
const elemSchema = node.schema.element!;
|
|
328
|
+
expect(elemSchema.kind).toBe(SchemaKind.RECORD);
|
|
329
|
+
expect(node.schema.attr["elemAttr"]).toBe("InnerRecord");
|
|
330
|
+
|
|
331
|
+
const expected = '<[//*fixed input*/ $listAttr="GlobalList" $elemAttr="InnerRecord"/ /*Missing $ prefix*/ /*comments2*/ id:number]>[(1)]';
|
|
332
|
+
assertRoundtrip(aidText, expected, false);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should handle meta schema field modifiers', () => {
|
|
336
|
+
/**
|
|
337
|
+
* Tests field modifiers inside a record definition: !required, $key=value.
|
|
338
|
+
*/
|
|
339
|
+
const aidText = `
|
|
340
|
+
<
|
|
341
|
+
/* comm0 */
|
|
342
|
+
/ $id=0 /*comm2 /* comm2.5*/ */ /
|
|
343
|
+
|
|
344
|
+
/* comm3 */
|
|
345
|
+
|
|
346
|
+
/* Modifiers block before field name */
|
|
347
|
+
!required $key=101 id:int,
|
|
348
|
+
|
|
349
|
+
$desc="User Name"
|
|
350
|
+
name: string
|
|
351
|
+
>
|
|
352
|
+
( /* comment0 */ / $id=3 /*comment2*/ / /*comment3*/ 1, "Alice" $id=65 #alice /*comment4*/ )
|
|
353
|
+
`;
|
|
354
|
+
|
|
355
|
+
const results = decode(aidText, { debug: false });
|
|
356
|
+
const node = results.node;
|
|
357
|
+
expect(results.errors).toHaveLength(0);
|
|
358
|
+
|
|
359
|
+
expect(node.isRecord).toBe(true);
|
|
360
|
+
|
|
361
|
+
// Retrieve field definitions from schema
|
|
362
|
+
const fields = node.schema.fields;
|
|
363
|
+
|
|
364
|
+
// Field 'id'
|
|
365
|
+
const fId = fields.find(f => f.name === "id")!;
|
|
366
|
+
expect(fId).toBeDefined();
|
|
367
|
+
expect(fId.required).toBe(true);
|
|
368
|
+
expect(fId.attr["key"]).toBe(101);
|
|
369
|
+
|
|
370
|
+
// Field 'name'
|
|
371
|
+
const fName = fields.find(f => f.name === "name")!;
|
|
372
|
+
expect(fName).toBeDefined();
|
|
373
|
+
expect(fName.required).toBe(false); // Default
|
|
374
|
+
expect(fName.attr["desc"]).toBe("User Name");
|
|
375
|
+
|
|
376
|
+
// Check Instance Data Meta (the node itself, not the schema)
|
|
377
|
+
// The record instance has / $id=3 /
|
|
378
|
+
expect(node.attr["id"]).toBe(3);
|
|
379
|
+
|
|
380
|
+
const expected = '<//*comm2 /* comm2.5*/*/ $id=0/ /*comm0*/ /*comm3*/ /*Modifiers block before field name*/ !required $key=101 id:number,$desc="User Name" name:string>(//*comment2*/ $id=3/ /*comment0*/ /*comment3*/ 1,/*comment4*/ $id=65 #alice "Alice")';
|
|
381
|
+
assertRoundtrip(aidText, expected, false);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
// ==============================================================================
|
|
386
|
+
// 2. DATA BLOCK META (Metadata inside data blocks [ ... ])
|
|
387
|
+
// ==============================================================================
|
|
388
|
+
|
|
389
|
+
it('should handle meta data block list primitive', () => {
|
|
390
|
+
/**
|
|
391
|
+
* Tests metadata inside a data block for a simple list.
|
|
392
|
+
* Syntax: [ / @size=3 / 1, 2, 3 ]
|
|
393
|
+
*/
|
|
394
|
+
const aidText = '[ / $size=3 $author="me" / 1, 2, 3 ]';
|
|
395
|
+
|
|
396
|
+
const results = decode(aidText, { debug: false });
|
|
397
|
+
const node = results.node;
|
|
398
|
+
expect(results.errors).toHaveLength(0);
|
|
399
|
+
|
|
400
|
+
expect(node.isList).toBe(true);
|
|
401
|
+
// Meta should go to this specific node's attributes
|
|
402
|
+
expect(node.attr["size"]).toBe(3);
|
|
403
|
+
expect(node.attr["author"]).toBe("me");
|
|
404
|
+
|
|
405
|
+
// Check content
|
|
406
|
+
expect(node.elements).toHaveLength(3);
|
|
407
|
+
expect(node.elements[0].value).toBe(1);
|
|
408
|
+
|
|
409
|
+
const expected = '<[number]>[/$size=3 $author="me"/ 1,2,3]';
|
|
410
|
+
assertRoundtrip(aidText, expected, false);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// ==============================================================================
|
|
414
|
+
// 3. NESTED META (Lists within lists)
|
|
415
|
+
// ==============================================================================
|
|
416
|
+
|
|
417
|
+
it('should handle meta nested lists', () => {
|
|
418
|
+
/**
|
|
419
|
+
* Tests metadata assignment in nested lists.
|
|
420
|
+
*/
|
|
421
|
+
const aidText = `
|
|
422
|
+
[
|
|
423
|
+
/ $level=0 /
|
|
424
|
+
[
|
|
425
|
+
/ $level=1 /
|
|
426
|
+
1, 2
|
|
427
|
+
],
|
|
428
|
+
[
|
|
429
|
+
/ $level=2 /
|
|
430
|
+
3, 4
|
|
431
|
+
]
|
|
432
|
+
]
|
|
433
|
+
`;
|
|
434
|
+
|
|
435
|
+
const results = decode(aidText, { debug: false });
|
|
436
|
+
const node = results.node;
|
|
437
|
+
expect(results.errors).toHaveLength(0);
|
|
438
|
+
|
|
439
|
+
// Root Node
|
|
440
|
+
expect(node.isList).toBe(true);
|
|
441
|
+
expect(node.attr["level"]).toBe(0);
|
|
442
|
+
|
|
443
|
+
// Inner Node 1
|
|
444
|
+
const inner1 = node.elements[0];
|
|
445
|
+
expect(inner1.isList).toBe(true);
|
|
446
|
+
expect(inner1.attr["level"]).toBe(1);
|
|
447
|
+
|
|
448
|
+
// Inner Node 2
|
|
449
|
+
const inner2 = node.elements[1];
|
|
450
|
+
expect(inner2.isList).toBe(true);
|
|
451
|
+
expect(inner2.attr["level"]).toBe(2);
|
|
452
|
+
|
|
453
|
+
const expected = '<[[number]]>[/$level=0/ [/$level=1/ 1,2],[/$level=2/ 3,4]]';
|
|
454
|
+
assertRoundtrip(aidText, expected, false);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// ==============================================================================
|
|
458
|
+
// 4. EDGE CASES & OVERRIDES
|
|
459
|
+
// ==============================================================================
|
|
460
|
+
|
|
461
|
+
it('should handle meta mixed with type override', () => {
|
|
462
|
+
/**
|
|
463
|
+
* Tests a scenario where we have metadata for the list AND a type override for an element.
|
|
464
|
+
*/
|
|
465
|
+
const aidText = '[ / $info="mixed" / 1, 2, <string> "3" ]';
|
|
466
|
+
const expected = '<[number]>[/$info="mixed"/ 1,2,<string> "3"]';
|
|
467
|
+
|
|
468
|
+
const result = decode(aidText, { debug: false });
|
|
469
|
+
const node = result.node;
|
|
470
|
+
expect(result.errors).toHaveLength(0);
|
|
471
|
+
|
|
472
|
+
// List Meta
|
|
473
|
+
expect(node.attr["info"]).toBe("mixed");
|
|
474
|
+
|
|
475
|
+
// List Type Inference (Should be Number based on first element '1')
|
|
476
|
+
expect(node.schema.element?.typeName).toBe("number");
|
|
477
|
+
|
|
478
|
+
// Element Override
|
|
479
|
+
const elLast = node.elements[2];
|
|
480
|
+
expect(elLast.schema.typeName).toBe("string");
|
|
481
|
+
expect(elLast.value).toBe("3");
|
|
482
|
+
|
|
483
|
+
assertRoundtrip(node, expected, false);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should handle meta and explicit type in data', () => {
|
|
487
|
+
/**
|
|
488
|
+
* Tests a scenario where an explicit type is provided inside the / ... / block.
|
|
489
|
+
* The parser must understand that type is the list type, and @tag is metadata.
|
|
490
|
+
*/
|
|
491
|
+
const aidText = '[ / $tag=1 / 1, 2 ]';
|
|
492
|
+
|
|
493
|
+
const result = decode(aidText, { debug: false });
|
|
494
|
+
const node = result.node;
|
|
495
|
+
expect(result.errors).toHaveLength(0);
|
|
496
|
+
|
|
497
|
+
expect(node.isList).toBe(true);
|
|
498
|
+
// Inferred type
|
|
499
|
+
expect(node.schema.element?.typeName).toBe("number");
|
|
500
|
+
expect(Object.keys(node.attr)).toHaveLength(1);
|
|
501
|
+
|
|
502
|
+
const expected = "<[number]>[/$tag=1/ 1,2]";
|
|
503
|
+
assertRoundtrip(node, expected, false);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
});
|
package/tests/utils.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { expect } from 'vitest';
|
|
2
|
+
import { Node } from '../src/models/Node';
|
|
3
|
+
// Zakładam, że masz te funkcje wyeksportowane z index.ts (szczegóły niżej)
|
|
4
|
+
import { decode, encode } from '../src/index';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validates encoding consistency (Round-Trip):
|
|
8
|
+
* 1. If source is text -> decode it to a Node.
|
|
9
|
+
* 2. Encode Node -> check if it matches expectedOutput.
|
|
10
|
+
* 3. Decode the result (encoded_txt) -> check if it remains a valid Node.
|
|
11
|
+
* 4. Re-encode -> check if the result is stable (idempotent).
|
|
12
|
+
* * Returns the Node so that further logical assertions (field checking) can be performed.
|
|
13
|
+
*/
|
|
14
|
+
export function assertRoundtrip(
|
|
15
|
+
source: string | Node | any,
|
|
16
|
+
expectedOutput: string,
|
|
17
|
+
debug: boolean = false
|
|
18
|
+
): Node {
|
|
19
|
+
let node: Node;
|
|
20
|
+
|
|
21
|
+
// 1. Prepare Node (if input is raw text)
|
|
22
|
+
if (typeof source === 'string') {
|
|
23
|
+
// Przekazujemy debug do decodera
|
|
24
|
+
const res = decode(source, { debug });
|
|
25
|
+
|
|
26
|
+
if (res.errors.length > 0) {
|
|
27
|
+
console.error("Input decoding errors:", res.errors);
|
|
28
|
+
}
|
|
29
|
+
expect(res.errors, `Input decoding errors: ${res.errors.join(', ')}`).toHaveLength(0);
|
|
30
|
+
node = res.node;
|
|
31
|
+
} else {
|
|
32
|
+
node = source;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. First Encoding
|
|
36
|
+
// Wymuszamy compact: true zgodnie z pythonowym oryginałem
|
|
37
|
+
const encoded1 = encode(node, { compact: true });
|
|
38
|
+
|
|
39
|
+
// Debug print to visualize differences in case of failure
|
|
40
|
+
// (Vitest zrobi to automatycznie przy .toBe, ale zachowujemy logikę z Python)
|
|
41
|
+
if (encoded1 !== expectedOutput) {
|
|
42
|
+
console.log(`\n[ROUNDTRIP] Mismatch Pass 1:`);
|
|
43
|
+
console.log(`EXPECTED: "${expectedOutput}"`);
|
|
44
|
+
console.log(`ACTUAL: "${encoded1}"`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
expect(encoded1).toBe(expectedOutput);
|
|
48
|
+
|
|
49
|
+
// 3. Round Trip (Decode the result of the encoding)
|
|
50
|
+
const res2 = decode(encoded1, { debug });
|
|
51
|
+
|
|
52
|
+
if (res2.errors.length > 0) {
|
|
53
|
+
console.error("Re-decoding errors:", res2.errors);
|
|
54
|
+
}
|
|
55
|
+
expect(res2.errors, `Re-decoding errors: ${res2.errors.join(', ')}`).toHaveLength(0);
|
|
56
|
+
|
|
57
|
+
// 4. Second Encoding (Idempotency Check)
|
|
58
|
+
const encoded2 = encode(res2.node, { compact: true });
|
|
59
|
+
|
|
60
|
+
if (encoded2 !== expectedOutput) {
|
|
61
|
+
console.log(`\n[ROUNDTRIP] Mismatch Pass 2 (Consistency):`);
|
|
62
|
+
console.log(`EXPECTED: "${expectedOutput}"`);
|
|
63
|
+
console.log(`ACTUAL: "${encoded2}"`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
expect(encoded2).toBe(expectedOutput);
|
|
67
|
+
|
|
68
|
+
return node;
|
|
69
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
/* --- Basic Language Configuration --- */
|
|
4
|
+
"target": "ES2020", /* JavaScript version to generate. ES2020 is safe for Node 14+ */
|
|
5
|
+
"module": "CommonJS", /* Module standard (Node.js uses CommonJS by default) */
|
|
6
|
+
"lib": ["ES2020"], /* Available libraries (e.g., Promise, Map, Set) */
|
|
7
|
+
"moduleResolution": "node", /* Module resolution strategy */
|
|
8
|
+
|
|
9
|
+
/* --- Output File Structure --- */
|
|
10
|
+
"outDir": "./dist", /* Where compiled files should go */
|
|
11
|
+
"rootDir": "./src", /* Source directory. Ensures strictly that only files inside src are compiled */
|
|
12
|
+
"declaration": true, /* Generate .d.ts files (required for npm libraries) */
|
|
13
|
+
"declarationMap": true, /* Allows editors (VS Code) to go to source definition */
|
|
14
|
+
"sourceMap": true, /* Generate source maps (.js.map) for debugging */
|
|
15
|
+
"removeComments": false, /* Do not remove comments (useful for JSDoc in IDEs) */
|
|
16
|
+
|
|
17
|
+
/* --- Interoperability --- */
|
|
18
|
+
"esModuleInterop": true, /* Allows importing CommonJS modules as default imports */
|
|
19
|
+
"forceConsistentCasingInFileNames": true, /* Enforce consistent casing in file names */
|
|
20
|
+
"resolveJsonModule": true, /* Allows importing .json files in code */
|
|
21
|
+
|
|
22
|
+
/* --- Strict Type-Checking --- */
|
|
23
|
+
"strict": true, /* Enable all strict type-checking options */
|
|
24
|
+
"noImplicitAny": true, /* Error if type is not explicit and is 'any' */
|
|
25
|
+
"strictNullChecks": true, /* Error on unhandled null/undefined */
|
|
26
|
+
"strictPropertyInitialization": true, /* Check if class properties are initialized in the constructor */
|
|
27
|
+
"noImplicitReturns": true, /* Every code path in a function must return a value */
|
|
28
|
+
"noFallthroughCasesInSwitch": true, /* Prevent accidental fallthrough in switch cases */
|
|
29
|
+
|
|
30
|
+
/* --- Code Cleanliness --- */
|
|
31
|
+
"noUnusedLocals": true, /* Report error for unused local variables */
|
|
32
|
+
"noUnusedParameters": true, /* Report error for unused function parameters */
|
|
33
|
+
|
|
34
|
+
/* --- Compilation Optimization --- */
|
|
35
|
+
"skipLibCheck": true /* Skip type checking of declaration files (.d.ts) in external libs */
|
|
36
|
+
},
|
|
37
|
+
"include": [
|
|
38
|
+
"src/**/*" /* Compile everything in the src folder */
|
|
39
|
+
],
|
|
40
|
+
"exclude": [
|
|
41
|
+
"node_modules",
|
|
42
|
+
"dist",
|
|
43
|
+
"**/*.test.ts", /* Do not include tests in the production build */
|
|
44
|
+
"**/*.spec.ts"
|
|
45
|
+
]
|
|
46
|
+
}
|