@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,108 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { encode, decode, MetaInfo, parse } from '../src/index';
|
|
3
|
+
import { assertRoundtrip } from './utils';
|
|
4
|
+
|
|
5
|
+
// ==================================================================================
|
|
6
|
+
// 1. DECODING TESTS (String -> Node)
|
|
7
|
+
// ==================================================================================
|
|
8
|
+
|
|
9
|
+
describe('AI Data Primitives', () => {
|
|
10
|
+
it('should encode raw object', () => {
|
|
11
|
+
const input = { foo: "bar" };
|
|
12
|
+
const expected = '<foo:string>("bar")';
|
|
13
|
+
assertRoundtrip(input, expected);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it.each([
|
|
17
|
+
// [Input Text, Expected Value, Expected Encoded Output]
|
|
18
|
+
["123", 123, "<number>123"],
|
|
19
|
+
["-50", -50, "<number>-50"],
|
|
20
|
+
['"hello"', "hello", '<string>"hello"'],
|
|
21
|
+
['"hello world"', "hello world", '<string>"hello world"'],
|
|
22
|
+
["true", true, "<bool>true"],
|
|
23
|
+
["false", false, "<bool>false"],
|
|
24
|
+
["null", null, "<null>null"],
|
|
25
|
+
])('should decode and encode primitive: %s', (text, expectedVal, expectedEnc) => {
|
|
26
|
+
// 1. DECODE
|
|
27
|
+
// We pass an options object (assuming the latest signature: decode(text, { debug: false }))
|
|
28
|
+
const res = decode(text, { debug: false });
|
|
29
|
+
|
|
30
|
+
expect(res.errors, `Parsing failed for input: ${text}`).toHaveLength(0);
|
|
31
|
+
expect(res.node.value).toBe(expectedVal);
|
|
32
|
+
expect(res.node.isPrimitive).toBe(true);
|
|
33
|
+
|
|
34
|
+
// 2. ENCODE
|
|
35
|
+
const encodedStr = encode(res.node, { colorize: false, compact: true }).trim();
|
|
36
|
+
|
|
37
|
+
expect(encodedStr).toBe(expectedEnc);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should decode floats', () => {
|
|
41
|
+
const cases: [string, number][] = [
|
|
42
|
+
["12.34", 12.34],
|
|
43
|
+
["-0.005", -0.005],
|
|
44
|
+
["0.0", 0.0]
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
cases.forEach(([text, expected]) => {
|
|
48
|
+
const res = decode(text);
|
|
49
|
+
expect(res.errors).toHaveLength(0);
|
|
50
|
+
expect(res.node.value).toBe(expected);
|
|
51
|
+
expect(typeof res.node.value).toBe('number');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should decode named record', () => {
|
|
56
|
+
const text = '{id: 1, name: "Test"}';
|
|
57
|
+
const res = decode(text, { debug: false });
|
|
58
|
+
expect(res.errors).toHaveLength(0);
|
|
59
|
+
|
|
60
|
+
const node = res.node;
|
|
61
|
+
|
|
62
|
+
expect(node.isRecord).toBe(true);
|
|
63
|
+
expect(node.fields["id"].value).toBe(1);
|
|
64
|
+
expect(node.fields["name"].value).toBe("Test");
|
|
65
|
+
|
|
66
|
+
assertRoundtrip(
|
|
67
|
+
node,
|
|
68
|
+
'<id:number,name:string>(1,"Test")',
|
|
69
|
+
false
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should decode positional record', () => {
|
|
74
|
+
const text = '(10, "Alice")';
|
|
75
|
+
const res = decode(text, { debug: false });
|
|
76
|
+
expect(res.errors).toHaveLength(0);
|
|
77
|
+
|
|
78
|
+
const node = res.node;
|
|
79
|
+
|
|
80
|
+
expect(node.isRecord).toBe(true);
|
|
81
|
+
|
|
82
|
+
// Verify if the parser mapped positional fields to _0, _1
|
|
83
|
+
expect(node.fields["_0"].value).toBe(10);
|
|
84
|
+
expect(node.fields["_1"].value).toBe("Alice");
|
|
85
|
+
|
|
86
|
+
assertRoundtrip(
|
|
87
|
+
node,
|
|
88
|
+
'<_0:number,_1:string>(10,"Alice")',
|
|
89
|
+
false
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should decode raw string', () => {
|
|
94
|
+
const text = "{color: red, status: active}";
|
|
95
|
+
const res = decode(text, { debug: false });
|
|
96
|
+
expect(res.errors).toHaveLength(0);
|
|
97
|
+
|
|
98
|
+
const node = res.node;
|
|
99
|
+
expect(node.fields["color"].value).toBe("red");
|
|
100
|
+
expect(node.fields["status"].value).toBe("active");
|
|
101
|
+
|
|
102
|
+
assertRoundtrip(
|
|
103
|
+
node,
|
|
104
|
+
'<color:string,status:string>("red","active")',
|
|
105
|
+
false
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Schema, SchemaKind } from '../src/index';
|
|
3
|
+
|
|
4
|
+
describe('AI Schema test', () => {
|
|
5
|
+
it('should encode raw object', () => {
|
|
6
|
+
const schema = new Schema(
|
|
7
|
+
SchemaKind.DICT,
|
|
8
|
+
{
|
|
9
|
+
name: "TestSchema",
|
|
10
|
+
comments: ["This is a comment"],
|
|
11
|
+
attr: { foo: "bar" },
|
|
12
|
+
tags: ["tag1", "tag2"],
|
|
13
|
+
required: true
|
|
14
|
+
|
|
15
|
+
});
|
|
16
|
+
const expected = '<Schema(DICT) name="TestSchema" !required attr=["foo"] tags=[tag1, tag2] comments=1>';
|
|
17
|
+
const expected_val = {
|
|
18
|
+
"comments": [
|
|
19
|
+
"This is a comment"
|
|
20
|
+
],
|
|
21
|
+
"attr": {
|
|
22
|
+
"foo": "bar"
|
|
23
|
+
},
|
|
24
|
+
"tags": [
|
|
25
|
+
"tag1",
|
|
26
|
+
"tag2"
|
|
27
|
+
],
|
|
28
|
+
"_fieldsList": [],
|
|
29
|
+
"_fieldsMap": {},
|
|
30
|
+
"kind": "DICT",
|
|
31
|
+
"typeName": "any",
|
|
32
|
+
"name": "TestSchema",
|
|
33
|
+
"element": null,
|
|
34
|
+
"key": null,
|
|
35
|
+
"value": null,
|
|
36
|
+
"required": true
|
|
37
|
+
}
|
|
38
|
+
expect(schema).toMatchObject(expected_val);
|
|
39
|
+
expect(schema.toString()).toBe(expected);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { decode } from '../src/index';
|
|
3
|
+
import { assertRoundtrip } from './utils';
|
|
4
|
+
|
|
5
|
+
// ==================================================================================
|
|
6
|
+
// 2. SCHEMA DEFINITION & TYPING
|
|
7
|
+
// ==================================================================================
|
|
8
|
+
|
|
9
|
+
describe('AI Data Schema Definitions', () => {
|
|
10
|
+
|
|
11
|
+
it('should handle schema definition and usage', () => {
|
|
12
|
+
/**
|
|
13
|
+
* Validates that a type defined with @Type<...> is correctly applied
|
|
14
|
+
* to the following value.
|
|
15
|
+
*/
|
|
16
|
+
// Define schema first, then use it explicitly
|
|
17
|
+
const fullText = '@User<id:int, name:string> @User(1, "Admin")';
|
|
18
|
+
const res = decode(fullText, { debug: false });
|
|
19
|
+
|
|
20
|
+
expect(res.errors).toHaveLength(0);
|
|
21
|
+
|
|
22
|
+
const node = res.node;
|
|
23
|
+
// Check if schema is linked correctly
|
|
24
|
+
expect(node.schema).not.toBeNull();
|
|
25
|
+
expect(node.schema?.typeName).toBe("User");
|
|
26
|
+
|
|
27
|
+
// Since we have a schema, positional arguments should be mapped to fields
|
|
28
|
+
// Check by key (Decoder maps positional to fields if schema exists)
|
|
29
|
+
expect(node.fields["id"].value).toBe(1);
|
|
30
|
+
expect(node.fields["name"].value).toBe("Admin");
|
|
31
|
+
|
|
32
|
+
assertRoundtrip(
|
|
33
|
+
node,
|
|
34
|
+
'@User<id:number,name:string>(1,"Admin")',
|
|
35
|
+
false
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should handle nested schema structure', () => {
|
|
40
|
+
/**
|
|
41
|
+
* Validates nested structural types.
|
|
42
|
+
*/
|
|
43
|
+
// We define Profile, then User uses it.
|
|
44
|
+
const text = `
|
|
45
|
+
@Profile<level:int>
|
|
46
|
+
@User<id:int, profile: @Profile>
|
|
47
|
+
@User(1, {level: 99})
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const res = decode(text, { debug: false });
|
|
51
|
+
|
|
52
|
+
expect(res.errors).toHaveLength(0);
|
|
53
|
+
const node = res.node;
|
|
54
|
+
|
|
55
|
+
// id should be 1
|
|
56
|
+
expect(node.fields["id"].value).toBe(1);
|
|
57
|
+
|
|
58
|
+
// profile should be a node
|
|
59
|
+
const profileNode = node.fields["profile"];
|
|
60
|
+
expect(profileNode).toBeDefined();
|
|
61
|
+
expect(profileNode.fields["level"].value).toBe(99);
|
|
62
|
+
|
|
63
|
+
assertRoundtrip(
|
|
64
|
+
node,
|
|
65
|
+
"@User<id:number,profile:@Profile<level:number>>(1,(99))",
|
|
66
|
+
false
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { encode, decode } from '../src/index';
|
|
3
|
+
import { assertRoundtrip } from './utils';
|
|
4
|
+
|
|
5
|
+
// ==================================================================================
|
|
6
|
+
// 3. ENCODING TESTS (JS Object -> String)
|
|
7
|
+
// ==================================================================================
|
|
8
|
+
|
|
9
|
+
describe('AI Data Encoding', () => {
|
|
10
|
+
|
|
11
|
+
it('should encode simple dict', () => {
|
|
12
|
+
/**
|
|
13
|
+
* Validates encoding a JS Object to AI.DATA format.
|
|
14
|
+
*/
|
|
15
|
+
const data = { x: 10, y: 20 };
|
|
16
|
+
|
|
17
|
+
const result = encode(data, { compact: true });
|
|
18
|
+
const expected = '<x:number,y:number>(10,20)';
|
|
19
|
+
|
|
20
|
+
expect(result).toBe(expected);
|
|
21
|
+
|
|
22
|
+
assertRoundtrip(
|
|
23
|
+
result,
|
|
24
|
+
expected,
|
|
25
|
+
false
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should encode list of objects', () => {
|
|
30
|
+
/**
|
|
31
|
+
* Validates encoding a list of objects.
|
|
32
|
+
*/
|
|
33
|
+
const data = [{ name: "A", val: 1 }, { name: "B", val: 2 }];
|
|
34
|
+
|
|
35
|
+
const result = encode(data, { compact: true, colorize: false });
|
|
36
|
+
const expected = '<[name:string,val:number]>[("A",1),("B",2)]';
|
|
37
|
+
|
|
38
|
+
expect(result).toBe(expected);
|
|
39
|
+
|
|
40
|
+
assertRoundtrip(
|
|
41
|
+
result,
|
|
42
|
+
expected,
|
|
43
|
+
false
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should handle round trip consistency', () => {
|
|
48
|
+
/**
|
|
49
|
+
* Golden Test: Encode -> Decode -> Compare.
|
|
50
|
+
*/
|
|
51
|
+
const originalData = [
|
|
52
|
+
{ id: 1, active: true, tags: ["a", "b"] },
|
|
53
|
+
{ id: 2, active: false, tags: ["c"] },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const expected = '<[id:number,active:bool,tags:[string]]>[(1,true,["a","b"]),(2,false,["c"])]';
|
|
57
|
+
|
|
58
|
+
// 1. Encode
|
|
59
|
+
// Note: converted snake_case config keys to camelCase for TS
|
|
60
|
+
const encodedText = encode(
|
|
61
|
+
originalData,
|
|
62
|
+
{ compact: true, includeSchema: true, colorize: false }
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(encodedText).toBe(expected);
|
|
66
|
+
assertRoundtrip(encodedText, expected, false);
|
|
67
|
+
|
|
68
|
+
// 2. Decode
|
|
69
|
+
const res = decode(encodedText, { debug: false });
|
|
70
|
+
|
|
71
|
+
// Optional: inspect schema structure
|
|
72
|
+
|
|
73
|
+
expect(res.errors).toHaveLength(0);
|
|
74
|
+
|
|
75
|
+
// 3. Convert back to JS object (equivalent to .dict() in Python)
|
|
76
|
+
const decodedData = res.node.dict()
|
|
77
|
+
|
|
78
|
+
// 4. Compare
|
|
79
|
+
expect(decodedData).toHaveLength(2);
|
|
80
|
+
expect(decodedData[0].id).toBe(1);
|
|
81
|
+
expect(decodedData[0].active).toBe(true);
|
|
82
|
+
expect(decodedData[0].tags).toEqual(["a", "b"]);
|
|
83
|
+
expect(decodedData[1].active).toBe(false);
|
|
84
|
+
|
|
85
|
+
// Deep equality check
|
|
86
|
+
expect(decodedData).toEqual(originalData);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { encode, decode } from '../src/index';
|
|
3
|
+
|
|
4
|
+
// ==================================================================================
|
|
5
|
+
// 4. ROUND TRIP (Encode -> Decode) with Errors
|
|
6
|
+
// ==================================================================================
|
|
7
|
+
|
|
8
|
+
describe('A Data Error Handling', () => {
|
|
9
|
+
|
|
10
|
+
it('should report errors on unstripped ANSI codes', () => {
|
|
11
|
+
/**
|
|
12
|
+
* Validates that the Decoder handles ANSI codes gracefully (by reporting errors)
|
|
13
|
+
* if they are not stripped out before decoding.
|
|
14
|
+
*/
|
|
15
|
+
const originalData = [{ id: 1, active: true }, { id: 2, active: false }];
|
|
16
|
+
|
|
17
|
+
// 1. Encode WITH colors
|
|
18
|
+
// Note: We use camelCase keys for config in TypeScript
|
|
19
|
+
const encodedText = encode(
|
|
20
|
+
originalData,
|
|
21
|
+
{
|
|
22
|
+
compact: true,
|
|
23
|
+
includeSchema: true,
|
|
24
|
+
colorize: true,
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// 2. Decode WITHOUT stripping colors (simulate user error)
|
|
29
|
+
// Pass removeAnsiColors: false explicitely
|
|
30
|
+
const res = decode(encodedText, { removeAnsiColors: false, debug: false });
|
|
31
|
+
|
|
32
|
+
// 3. Assertions
|
|
33
|
+
expect(res.errors.length).toBeGreaterThan(0); // Decoder should report errors on raw ANSI codes
|
|
34
|
+
|
|
35
|
+
// Optional: Verify error content
|
|
36
|
+
const messages = res.errors.map(e => e.message);
|
|
37
|
+
const hasUnexpectedChar = messages.some(m => m.includes("Unexpected character"));
|
|
38
|
+
expect(hasUnexpectedChar).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ==================================================================================
|
|
42
|
+
// 5. PARSING ERROR SCENARIOS
|
|
43
|
+
// ==================================================================================
|
|
44
|
+
|
|
45
|
+
it('should fail on unclosed list', () => {
|
|
46
|
+
/**
|
|
47
|
+
* Ensures parsing fails for malformed lists.
|
|
48
|
+
*/
|
|
49
|
+
const text = "[1, 2, 3"; // Missing closing bracket
|
|
50
|
+
const res = decode(text, { debug: false });
|
|
51
|
+
|
|
52
|
+
expect(res.errors.length).toBeGreaterThan(0);
|
|
53
|
+
|
|
54
|
+
const msg = res.errors[0].message;
|
|
55
|
+
// Check for typical error messages related to missing tokens or EOF
|
|
56
|
+
const isExpectedError = msg.includes("Expected") || msg.includes("got") || msg.includes("EOF");
|
|
57
|
+
expect(isExpectedError).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should fail on unexpected character', () => {
|
|
61
|
+
/**
|
|
62
|
+
* Ensures parsing fails for illegal characters.
|
|
63
|
+
*/
|
|
64
|
+
const text = "(1, ?)";
|
|
65
|
+
const res = decode(text, { debug: false });
|
|
66
|
+
|
|
67
|
+
expect(res.errors.length).toBeGreaterThan(0);
|
|
68
|
+
expect(res.errors[0].message).toContain("Unexpected character");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { encode, decode, parse, SchemaKind } from '../src/index';
|
|
3
|
+
import { assertRoundtrip } from './utils';
|
|
4
|
+
|
|
5
|
+
describe('AI Data List Handling', () => {
|
|
6
|
+
|
|
7
|
+
it('should decode list of primitives', () => {
|
|
8
|
+
/**
|
|
9
|
+
* Validates simple lists.
|
|
10
|
+
*/
|
|
11
|
+
const text = "[1, 2, 3]";
|
|
12
|
+
const expected = "<[number]>[1,2,3]";
|
|
13
|
+
|
|
14
|
+
const res = decode(text, { debug: false });
|
|
15
|
+
expect(res.errors).toHaveLength(0);
|
|
16
|
+
|
|
17
|
+
const node = res.node;
|
|
18
|
+
// Check elements (Node logic stores simple lists in elements)
|
|
19
|
+
const values = node.elements.map(el => el.value);
|
|
20
|
+
expect(values).toEqual([1, 2, 3]);
|
|
21
|
+
|
|
22
|
+
assertRoundtrip(node, expected, false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should infer mixed list type from first element', () => {
|
|
26
|
+
/**
|
|
27
|
+
* Tests if the list type is inferred based on the first element ("a" -> string).
|
|
28
|
+
* As a result, the number 3 (int) should be treated as a mismatch and marked with the <number> tag.
|
|
29
|
+
*/
|
|
30
|
+
// 1. Input data: List starts with strings but has an int at the end
|
|
31
|
+
const data = { tests: ["a", "b", "c", 3] };
|
|
32
|
+
|
|
33
|
+
// 2. Encoding (inference happens here in Parser.ts)
|
|
34
|
+
const node = parse(data);
|
|
35
|
+
|
|
36
|
+
expect(node.schema).not.toBeNull();
|
|
37
|
+
expect(node.schema.isRecord).toBe(true);
|
|
38
|
+
expect(node.fields).toHaveProperty("tests");
|
|
39
|
+
|
|
40
|
+
const testsNode = node.fields["tests"];
|
|
41
|
+
expect(testsNode.isList).toBe(true);
|
|
42
|
+
expect(testsNode.schema).not.toBeNull();
|
|
43
|
+
expect(testsNode.schema.element).not.toBeNull();
|
|
44
|
+
|
|
45
|
+
expect(testsNode.schema.element?.kind).toBe(SchemaKind.PRIMITIVE);
|
|
46
|
+
expect(testsNode.schema.element?.typeName).toBe("string"); // Inferred from first element "a"
|
|
47
|
+
|
|
48
|
+
expect(testsNode.elements).toHaveLength(4);
|
|
49
|
+
|
|
50
|
+
const output = encode(data, { compact: true, colorize: false });
|
|
51
|
+
|
|
52
|
+
// We expect the list NOT to be [any], but [string] (implied or explicit),
|
|
53
|
+
// so strings will be "clean", and the number will get a tag.
|
|
54
|
+
|
|
55
|
+
// Check if 'a' is treated normally (as a string in a string list)
|
|
56
|
+
expect(output).toContain('"a"');
|
|
57
|
+
|
|
58
|
+
// KEY: Check if 3 got a tag because it doesn't match the inferred String type
|
|
59
|
+
// TS Encoder outputs <number>
|
|
60
|
+
expect(output).toContain("<number> 3");
|
|
61
|
+
|
|
62
|
+
// Ensure there is NO tag next to strings (because they match the list type)
|
|
63
|
+
expect(output).not.toContain('<string> "a"');
|
|
64
|
+
|
|
65
|
+
const expected = '<tests:[string]>(["a","b","c",<number> 3])';
|
|
66
|
+
assertRoundtrip(node, expected, false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should generate tags for explicit any list due to primitive mismatch', () => {
|
|
70
|
+
/**
|
|
71
|
+
* Tests the scenario where a list is defined as [any].
|
|
72
|
+
* * Behavior:
|
|
73
|
+
* 1. 'any' is parsed as a PRIMITIVE type named "any".
|
|
74
|
+
* 2. The Decoder updates the schema based on the first element found ("a" -> string).
|
|
75
|
+
* 3. Because "string" != "number" (for value 3), the Encoder sees a mismatch and adds explicit tags.
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
// 1. Input in AID format
|
|
79
|
+
const aidText = `
|
|
80
|
+
<tests: [any]>
|
|
81
|
+
(
|
|
82
|
+
["a", "b", "c", 3]
|
|
83
|
+
)
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
// 2. Decode
|
|
87
|
+
const result = decode(aidText, { debug: false });
|
|
88
|
+
const node = result.node;
|
|
89
|
+
expect(result.errors).toHaveLength(0);
|
|
90
|
+
|
|
91
|
+
// 3. Verify Internal State
|
|
92
|
+
const testsNode = node.fields["tests"];
|
|
93
|
+
|
|
94
|
+
// Verify the list definition
|
|
95
|
+
const elementSchema = testsNode.schema.element;
|
|
96
|
+
expect(elementSchema?.kind).toBe(SchemaKind.PRIMITIVE);
|
|
97
|
+
|
|
98
|
+
// Decoder logic updates "any" element schema based on the first item found
|
|
99
|
+
expect(elementSchema?.typeName).toBe("string");
|
|
100
|
+
|
|
101
|
+
// Verify the actual elements
|
|
102
|
+
expect(testsNode.elements[0].schema.typeName).toBe("string");
|
|
103
|
+
expect(testsNode.elements[3].schema.typeName).toBe("number");
|
|
104
|
+
|
|
105
|
+
// 4. Encode
|
|
106
|
+
const expected = '<tests:[string]>(["a","b","c",<number> 3])';
|
|
107
|
+
assertRoundtrip(node, expected, false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle inference happy path', () => {
|
|
111
|
+
/**
|
|
112
|
+
* Theory: If a list has no type (or [any]), the first element ("a")
|
|
113
|
+
* should refine the list schema to [string].
|
|
114
|
+
*/
|
|
115
|
+
const dataStr = '["a", "b"]'; // No header = SchemaKind.ANY
|
|
116
|
+
|
|
117
|
+
const result = decode(dataStr, { debug: false });
|
|
118
|
+
const node = result.node;
|
|
119
|
+
expect(result.errors).toHaveLength(0);
|
|
120
|
+
|
|
121
|
+
expect(node.isList).toBe(true);
|
|
122
|
+
expect(node.schema.element?.typeName).toBe("string"); // Inferred!
|
|
123
|
+
|
|
124
|
+
const expected = '<[string]>["a","b"]';
|
|
125
|
+
assertRoundtrip(node, expected, false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should handle inference mismatch (string vs number)', () => {
|
|
129
|
+
/**
|
|
130
|
+
* Theory: First element "a" sets list to [string].
|
|
131
|
+
* The number 3 is mismatch and gets tagged.
|
|
132
|
+
*/
|
|
133
|
+
const dataStr = '["a", 3]';
|
|
134
|
+
|
|
135
|
+
const result = decode(dataStr, { debug: false });
|
|
136
|
+
const node = result.node;
|
|
137
|
+
expect(result.errors).toHaveLength(0);
|
|
138
|
+
|
|
139
|
+
// The list should have become [string] due to "a"
|
|
140
|
+
expect(node.schema.element?.typeName).toBe("string");
|
|
141
|
+
|
|
142
|
+
const expected = '<[string]>["a",<number> 3]';
|
|
143
|
+
assertRoundtrip(node, expected, false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should handle inference mismatch (number vs string)', () => {
|
|
147
|
+
/**
|
|
148
|
+
* Theory: First element 3 sets list to [number].
|
|
149
|
+
* The string "a" is mismatch and gets tagged.
|
|
150
|
+
*/
|
|
151
|
+
const dataStr = '[3, "a"]';
|
|
152
|
+
|
|
153
|
+
const result = decode(dataStr, { debug: false });
|
|
154
|
+
const node = result.node;
|
|
155
|
+
expect(result.errors).toHaveLength(0);
|
|
156
|
+
|
|
157
|
+
// The list should have become [number] due to 3
|
|
158
|
+
expect(node.schema.element?.typeName).toBe("number");
|
|
159
|
+
|
|
160
|
+
const expected = '<[number]>[3,<string> "a"]';
|
|
161
|
+
assertRoundtrip(node, expected, false);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should fix schema crash on override logic', () => {
|
|
165
|
+
/**
|
|
166
|
+
* Theory: We expect a String, but we get a List.
|
|
167
|
+
* This triggers 'needs_override' logic in the Encoder.
|
|
168
|
+
*/
|
|
169
|
+
// Header says 'test' is a string, but body has a list
|
|
170
|
+
const aidText = `
|
|
171
|
+
<test: string>
|
|
172
|
+
(
|
|
173
|
+
["a", "b"]
|
|
174
|
+
)
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
const result = decode(aidText, { debug: false });
|
|
178
|
+
const node = result.node;
|
|
179
|
+
expect(result.errors).toHaveLength(0);
|
|
180
|
+
|
|
181
|
+
// The Encoder sees a List Node vs String Schema -> Mismatch -> Adds Tag
|
|
182
|
+
const expected = '<test:string>(<[string]> ["a","b"])';
|
|
183
|
+
assertRoundtrip(node, expected, false);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should handle primitive list with implicit output', () => {
|
|
187
|
+
const aidText = `
|
|
188
|
+
<ab>
|
|
189
|
+
{
|
|
190
|
+
ab: ["a", "b", "c", 3]
|
|
191
|
+
}
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
const result = decode(aidText, { debug: false });
|
|
195
|
+
const node = result.node;
|
|
196
|
+
expect(result.errors).toHaveLength(0);
|
|
197
|
+
|
|
198
|
+
const expected = '<ab:[string]>(["a","b","c",<number> 3])';
|
|
199
|
+
assertRoundtrip(node, expected, false);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should handle simple mixed types', () => {
|
|
203
|
+
const aidText = '["a", "b", "c", 3]';
|
|
204
|
+
|
|
205
|
+
const result = decode(aidText, { debug: false });
|
|
206
|
+
const node = result.node;
|
|
207
|
+
expect(result.errors).toHaveLength(0);
|
|
208
|
+
|
|
209
|
+
const expected = '<[string]>["a","b","c",<number> 3]';
|
|
210
|
+
assertRoundtrip(node, expected, false);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should handle inner list types', () => {
|
|
214
|
+
// <[[int]]>[[2,3,4],[5,6,7]]
|
|
215
|
+
const aidText = '<[[int]]>[[2,3,4],[5,6,7]]';
|
|
216
|
+
|
|
217
|
+
const result = decode(aidText, { debug: false });
|
|
218
|
+
const node = result.node;
|
|
219
|
+
expect(result.errors).toHaveLength(0);
|
|
220
|
+
|
|
221
|
+
// 'int' normalizes to 'number' in the TS implementation
|
|
222
|
+
const expected = '<[[number]]>[[2,3,4],[5,6,7]]';
|
|
223
|
+
assertRoundtrip(node, expected, false);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { decode } from '../src/index';
|
|
3
|
+
import { assertRoundtrip } from './utils';
|
|
4
|
+
|
|
5
|
+
describe('AI Data Record Handling', () => {
|
|
6
|
+
|
|
7
|
+
// ==============================================================================
|
|
8
|
+
// 1. SCHEMA DEFINITION META (Types defined in < ... >)
|
|
9
|
+
// ==============================================================================
|
|
10
|
+
|
|
11
|
+
it('should handle different type in record', () => {
|
|
12
|
+
const aidText = `
|
|
13
|
+
<
|
|
14
|
+
id: number
|
|
15
|
+
>
|
|
16
|
+
( ["text"] )
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const result = decode(aidText, { debug: false });
|
|
20
|
+
const node = result.node;
|
|
21
|
+
expect(result.errors).toHaveLength(0);
|
|
22
|
+
|
|
23
|
+
// 1. Check Record Meta (Outer)
|
|
24
|
+
expect(node.isRecord).toBe(true);
|
|
25
|
+
|
|
26
|
+
const expected = '<id:number>(<[string]> ["text"])';
|
|
27
|
+
assertRoundtrip(node, expected, false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should handle simple types', () => {
|
|
31
|
+
// Input is a raw AID format string representing a dictionary
|
|
32
|
+
const aidText = '{ a:"a", b:"b", c:"c", d: 3 }';
|
|
33
|
+
|
|
34
|
+
// The parser infers the schema from the keys/values.
|
|
35
|
+
// The encoder then outputs the inferred schema and converts the named record
|
|
36
|
+
// to a positional one because a schema exists.
|
|
37
|
+
const expected = '<a:string,b:string,c:string,d:number>("a","b","c",3)';
|
|
38
|
+
|
|
39
|
+
assertRoundtrip(aidText, expected, false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should handle record named type mismatch', () => {
|
|
43
|
+
/**
|
|
44
|
+
* Tests a scenario where a record field has a defined type (e.g., string),
|
|
45
|
+
* but the actual value inside is of a different type (e.g., int).
|
|
46
|
+
*
|
|
47
|
+
* This ensures that the encodeRecord method uses applyTypeTag correctly.
|
|
48
|
+
*
|
|
49
|
+
* Schema: <tests: string>
|
|
50
|
+
* Data: { tests: 3 }
|
|
51
|
+
* Expected Output: ... (<number> 3)
|
|
52
|
+
*/
|
|
53
|
+
const aidText = `
|
|
54
|
+
<tests: string>
|
|
55
|
+
{
|
|
56
|
+
tests: 3
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
const expected = '<tests:string>(<number> 3)';
|
|
61
|
+
assertRoundtrip(aidText, expected, false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should handle record positional type mismatch', () => {
|
|
65
|
+
/**
|
|
66
|
+
* Tests a scenario where a positional record field has a defined type (e.g., string),
|
|
67
|
+
* but the actual value inside is of a different type (e.g., int).
|
|
68
|
+
*
|
|
69
|
+
* Schema: <tests: string>
|
|
70
|
+
* Data: (3)
|
|
71
|
+
* Expected Output: ... (<number> 3)
|
|
72
|
+
*/
|
|
73
|
+
const aidText = `
|
|
74
|
+
<tests: string>
|
|
75
|
+
(3)
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
const expected = '<tests:string>(<number> 3)';
|
|
79
|
+
assertRoundtrip(aidText, expected, false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
});
|