Skip to content

Commit 46e615c

Browse files
bulentvmehmetb
andcommitted
[TS] Add support for fixed length arrays on Typescript (#5864) (#7021)
* Typescript / Javascript don't have fixed arrays but it is important to support these languages for compatibility. * Generated TS code checks the length of the given array and do truncating / padding to conform to the schema. * Supports the both standard API and Object Based API. * Added a test. Co-authored-by: Mehmet Baker <[email protected]> Signed-off-by: Bulent Vural <[email protected]>
1 parent f7b7344 commit 46e615c

File tree

8 files changed

+631
-16
lines changed

8 files changed

+631
-16
lines changed

src/idl_gen_ts.cpp

Lines changed: 302 additions & 11 deletions
Large diffs are not rendered by default.

src/idl_parser.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2581,7 +2581,7 @@ bool Parser::SupportsAdvancedArrayFeatures() const {
25812581
return (opts.lang_to_generate &
25822582
~(IDLOptions::kCpp | IDLOptions::kPython | IDLOptions::kJava |
25832583
IDLOptions::kCSharp | IDLOptions::kJsonSchema | IDLOptions::kJson |
2584-
IDLOptions::kBinary | IDLOptions::kRust)) == 0;
2584+
IDLOptions::kBinary | IDLOptions::kRust | IDLOptions::kTs)) == 0;
25852585
}
25862586

25872587
Namespace *Parser::UniqueNamespace(Namespace *ns) {
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/* global BigInt */
2+
3+
import assert from 'assert';
4+
import { readFileSync, writeFileSync } from 'fs';
5+
import * as flatbuffers from 'flatbuffers';
6+
import {
7+
ArrayStructT,
8+
ArrayTable,
9+
ArrayTableT,
10+
InnerStructT,
11+
NestedStructT,
12+
OuterStructT,
13+
TestEnum,
14+
} from './arrays_test_complex/arrays_test_complex_generated.js';
15+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
16+
BigInt.prototype.toJSON = function () {
17+
return this.toString();
18+
};
19+
function fbObjToObj(fbObj) {
20+
const ret = {};
21+
for (const propName of Object.keys(fbObj)) {
22+
const key = propName;
23+
const prop = fbObj[key];
24+
if (prop.valueOf) {
25+
ret[key] = prop.valueOf();
26+
} else if (typeof prop === 'object') {
27+
ret[key] = fbObjToObj(prop);
28+
}
29+
}
30+
return ret;
31+
}
32+
function testBuild(monFile, jsFile) {
33+
const arrayTable = new ArrayTableT(
34+
'Complex Array Test',
35+
new ArrayStructT(
36+
221.139008,
37+
[-700, -600, -500, -400, -300, -200, -100, 0, 100, 200, 300, 400, 500, 600, 700],
38+
13,
39+
[
40+
new NestedStructT(
41+
[233, -123],
42+
TestEnum.B,
43+
[TestEnum.A, TestEnum.C],
44+
[
45+
new OuterStructT(
46+
false,
47+
123.456,
48+
new InnerStructT(
49+
123456792.0,
50+
[13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1],
51+
91,
52+
BigInt('9007199254740999')
53+
),
54+
[
55+
new InnerStructT(
56+
-987654321.9876,
57+
[255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243],
58+
123,
59+
BigInt('9007199254741000')
60+
),
61+
new InnerStructT(
62+
123000987.9876,
63+
[101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113],
64+
-123,
65+
BigInt('9007199254741000')
66+
),
67+
],
68+
new InnerStructT(
69+
987654321.9876,
70+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
71+
19,
72+
BigInt('9007199254741000')
73+
),
74+
[111000111.222, 222000222.111, 333000333.333, 444000444.444]
75+
),
76+
]
77+
),
78+
],
79+
-123456789
80+
)
81+
);
82+
const builder = new flatbuffers.Builder();
83+
builder.finish(arrayTable.pack(builder));
84+
if (jsFile) {
85+
const obj = fbObjToObj(arrayTable);
86+
writeFileSync(jsFile, `export default ${JSON.stringify(obj, null, 2)}`);
87+
}
88+
if (monFile) {
89+
writeFileSync(monFile, builder.asUint8Array());
90+
}
91+
return builder.asUint8Array();
92+
}
93+
function testParse(monFile, jsFile, buffer) {
94+
if (!buffer) {
95+
if (!monFile) {
96+
console.log(`Please specify mon file read the buffer from.`);
97+
process.exit(1);
98+
}
99+
buffer = readFileSync(monFile);
100+
}
101+
const byteBuffer = new flatbuffers.ByteBuffer(new Uint8Array(buffer));
102+
const arrayTable = ArrayTable.getRootAsArrayTable(byteBuffer).unpack();
103+
const json = JSON.stringify(arrayTable, null, 2);
104+
if (jsFile) {
105+
writeFileSync(jsFile, `export default ${json}`);
106+
}
107+
return arrayTable;
108+
}
109+
if (process.argv[2] === 'build') {
110+
testBuild(process.argv[3], process.argv[4]);
111+
} else if (process.argv[2] === 'parse') {
112+
testParse(process.argv[3], process.argv[4], null);
113+
} else {
114+
const arr = testBuild(null, null);
115+
const parsed = testParse(null, null, Buffer.from(arr));
116+
assert.strictEqual(parsed.a, 'Complex Array Test', 'String Test');
117+
assert.strictEqual(parsed?.cUnderscore?.aUnderscore, 221.13900756835938, 'Float Test');
118+
assert.deepEqual(parsed?.cUnderscore?.bUnderscore, [-700, -600, -500, -400, -300, -200, -100, 0, 100, 200, 300, 400, 500, 600, 700], 'Array of signed integers');
119+
assert.strictEqual(parsed?.cUnderscore.d?.[0].dOuter[0].d[1].a, 123000987.9876, 'Float in deep');
120+
assert.deepEqual(parsed?.cUnderscore?.d[0].dOuter?.[0]?.e, {
121+
a: 987654321.9876,
122+
b: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
123+
c: 19,
124+
dUnderscore: '9007199254741000',
125+
}, 'Object in deep');
126+
assert.deepEqual(parsed?.cUnderscore.g, ['0', '0'], 'Last object');
127+
128+
console.log('Arrays test: completed successfully');
129+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/* global BigInt */
2+
3+
import assert from 'assert';
4+
import { readFileSync, writeFileSync } from 'fs';
5+
import * as flatbuffers from 'flatbuffers';
6+
import {
7+
ArrayStructT,
8+
ArrayTable,
9+
ArrayTableT,
10+
InnerStructT,
11+
NestedStructT,
12+
OuterStructT,
13+
TestEnum,
14+
} from './arrays_test_complex/arrays_test_complex_generated.js';
15+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
16+
BigInt.prototype.toJSON = function () {
17+
return this.toString();
18+
};
19+
function fbObjToObj(fbObj) {
20+
const ret = {};
21+
for (const propName of Object.keys(fbObj)) {
22+
const key = propName;
23+
const prop = fbObj[key];
24+
if (prop.valueOf) {
25+
ret[key] = prop.valueOf();
26+
} else if (typeof prop === 'object') {
27+
ret[key] = fbObjToObj(prop);
28+
}
29+
}
30+
return ret;
31+
}
32+
function testBuild(monFile, jsFile) {
33+
const arrayTable = new ArrayTableT(
34+
'Complex Array Test',
35+
new ArrayStructT(
36+
221.139008,
37+
[-700, -600, -500, -400, -300, -200, -100, 0, 100, 200, 300, 400, 500, 600, 700],
38+
13,
39+
[
40+
new NestedStructT(
41+
[233, -123],
42+
TestEnum.B,
43+
[TestEnum.A, TestEnum.C],
44+
[
45+
new OuterStructT(
46+
false,
47+
123.456,
48+
new InnerStructT(
49+
123456792.0,
50+
[13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1],
51+
91,
52+
BigInt('9007199254740999')
53+
),
54+
[
55+
new InnerStructT(
56+
-987654321.9876,
57+
[255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243],
58+
123,
59+
BigInt('9007199254741000')
60+
),
61+
new InnerStructT(
62+
123000987.9876,
63+
[101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113],
64+
-123,
65+
BigInt('9007199254741000')
66+
),
67+
],
68+
new InnerStructT(
69+
987654321.9876,
70+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
71+
19,
72+
BigInt('9007199254741000')
73+
),
74+
[111000111.222, 222000222.111, 333000333.333, 444000444.444]
75+
),
76+
]
77+
),
78+
],
79+
-123456789
80+
)
81+
);
82+
const builder = new flatbuffers.Builder();
83+
builder.finish(arrayTable.pack(builder));
84+
if (jsFile) {
85+
const obj = fbObjToObj(arrayTable);
86+
writeFileSync(jsFile, `export default ${JSON.stringify(obj, null, 2)}`);
87+
}
88+
if (monFile) {
89+
writeFileSync(monFile, builder.asUint8Array());
90+
}
91+
return builder.asUint8Array();
92+
}
93+
function testParse(monFile, jsFile, buffer) {
94+
if (!buffer) {
95+
if (!monFile) {
96+
console.log(`Please specify mon file read the buffer from.`);
97+
process.exit(1);
98+
}
99+
buffer = readFileSync(monFile);
100+
}
101+
const byteBuffer = new flatbuffers.ByteBuffer(new Uint8Array(buffer));
102+
const arrayTable = ArrayTable.getRootAsArrayTable(byteBuffer).unpack();
103+
const json = JSON.stringify(arrayTable, null, 2);
104+
if (jsFile) {
105+
writeFileSync(jsFile, `export default ${json}`);
106+
}
107+
return arrayTable;
108+
}
109+
if (process.argv[2] === 'build') {
110+
testBuild(process.argv[3], process.argv[4]);
111+
} else if (process.argv[2] === 'parse') {
112+
testParse(process.argv[3], process.argv[4], null);
113+
} else {
114+
const arr = testBuild(null, null);
115+
const parsed = testParse(null, null, Buffer.from(arr));
116+
assert.strictEqual(parsed.a, 'Complex Array Test', 'String Test');
117+
assert.strictEqual(parsed?.cUnderscore?.aUnderscore, 221.13900756835938, 'Float Test');
118+
assert.deepEqual(parsed?.cUnderscore?.bUnderscore, [-700, -600, -500, -400, -300, -200, -100, 0, 100, 200, 300, 400, 500, 600, 700], 'Array of signed integers');
119+
assert.strictEqual(parsed?.cUnderscore.d?.[0].dOuter[0].d[1].a, 123000987.9876, 'Float in deep');
120+
assert.deepEqual(parsed?.cUnderscore?.d[0].dOuter?.[0]?.e, {
121+
a: 987654321.9876,
122+
b: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
123+
c: 19,
124+
dUnderscore: '9007199254741000',
125+
}, 'Object in deep');
126+
assert.deepEqual(parsed?.cUnderscore.g, ['0', '0'], 'Last object');
127+
128+
console.log('Arrays test: completed successfully');
129+
}

tests/ts/TypeScriptTest.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,18 @@ def flatc(options, schema, prefix=None, include=None, data=None, cwd=tests_path)
9595
include="../../",
9696
)
9797

98+
flatc(
99+
options=["-b", "--schema"],
100+
schema="arrays_test_complex/arrays_test_complex.fbs",
101+
prefix="arrays_test_complex"
102+
)
103+
104+
flatc(
105+
options=["--ts", "--reflect-names", "--ts-flat-files", "--gen-name-strings", "--gen-object-api"],
106+
schema="arrays_test_complex/arrays_test_complex.bfbs",
107+
prefix="arrays_test_complex"
108+
)
109+
98110
flatc(
99111
options=[
100112
"--ts",
@@ -121,4 +133,5 @@ def flatc(options, schema, prefix=None, include=None, data=None, cwd=tests_path)
121133
print("Running TypeScript Tests...")
122134
check_call(NODE_CMD + ["JavaScriptTest"])
123135
check_call(NODE_CMD + ["JavaScriptUnionVectorTest"])
124-
check_call(NODE_CMD + ["JavaScriptFlexBuffersTest"])
136+
check_call(NODE_CMD + ["JavaScriptFlexBuffersTest"])
137+
check_call(NODE_CMD + ["JavaScriptComplexArraysTest"])
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
namespace MyGame.Example;
2+
3+
// it appears that the library has already a problem with Enums
4+
// when generating ts file with '--ts-flat-files' from a fbs.
5+
// bfbs is fine.
6+
// workaround is to generate bfbs from fbs first, and then
7+
// generate flat .ts from bfbs if you have enum(s) in your chema
8+
9+
enum TestEnum : byte { A, B, C }
10+
11+
struct InnerStruct {
12+
a:float64;
13+
b:[ubyte:13];
14+
c:int8;
15+
d_underscore:int64;
16+
}
17+
18+
struct OuterStruct {
19+
a:bool;
20+
b:double;
21+
c_underscore:InnerStruct;
22+
d:[InnerStruct:3];
23+
e:InnerStruct;
24+
f:[float64:4];
25+
}
26+
27+
struct NestedStruct{
28+
a:[int:2];
29+
b:TestEnum;
30+
c_underscore:[TestEnum:2];
31+
d_outer:[OuterStruct:5];
32+
e:[int64:2];
33+
}
34+
35+
struct ArrayStruct{
36+
a_underscore:float;
37+
b_underscore:[int:0xF];
38+
c:byte;
39+
d:[NestedStruct:2];
40+
e:int32;
41+
f:[OuterStruct:2];
42+
g:[int64:2];
43+
}
44+
45+
table ArrayTable{
46+
a:string;
47+
c_underscore:ArrayStruct;
48+
}
49+
50+
root_type ArrayTable;
51+
file_identifier "RHUB";
52+
file_extension "mon";

tests/ts/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"typescript/**/*.ts",
2121
"optional_scalars/**/*.ts",
2222
"namespace_test/**/*.ts",
23-
"union_vector/**/*.ts"
23+
"union_vector/**/*.ts",
24+
"arrays_test_complex/**/*.ts"
2425
]
2526
}

tests/ts/union_vector/movie.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ unpack(): MovieT {
146146
})(),
147147
this.bb!.createScalarList<Character>(this.charactersType.bind(this), this.charactersTypeLength()),
148148
(() => {
149-
const ret = [];
149+
const ret: (AttackerT|BookReaderT|RapunzelT|string)[] = [];
150150
for(let targetEnumIndex = 0; targetEnumIndex < this.charactersTypeLength(); ++targetEnumIndex) {
151151
const targetEnum = this.charactersType(targetEnumIndex);
152152
if(targetEnum === null || Character[targetEnum!] === 'NONE') { continue; }
@@ -172,7 +172,7 @@ unpackTo(_o: MovieT): void {
172172
})();
173173
_o.charactersType = this.bb!.createScalarList<Character>(this.charactersType.bind(this), this.charactersTypeLength());
174174
_o.characters = (() => {
175-
const ret = [];
175+
const ret: (AttackerT|BookReaderT|RapunzelT|string)[] = [];
176176
for(let targetEnumIndex = 0; targetEnumIndex < this.charactersTypeLength(); ++targetEnumIndex) {
177177
const targetEnum = this.charactersType(targetEnumIndex);
178178
if(targetEnum === null || Character[targetEnum!] === 'NONE') { continue; }

0 commit comments

Comments
 (0)