Add typescript flatbuffer reflection library
Provide enough of a library to support the equivalent of our C++
FlatbufferToJson code.
Change-Id: I430884816c275d7901c88ad5406ed299e1eb00fa
diff --git a/aos/BUILD b/aos/BUILD
index 2bb1d50..a7d691b 100644
--- a/aos/BUILD
+++ b/aos/BUILD
@@ -281,6 +281,7 @@
flatbuffer_cc_library(
name = "configuration_fbs",
srcs = ["configuration.fbs"],
+ gen_reflections = 1,
visibility = ["//visibility:public"],
)
@@ -335,10 +336,17 @@
visibility = ["//visibility:public"],
)
+flatbuffer_ts_library(
+ name = "json_to_flatbuffer_flatbuffer_ts",
+ srcs = ["json_to_flatbuffer.fbs"],
+ visibility = ["//aos:__subpackages__"],
+)
+
flatbuffer_cc_library(
name = "json_to_flatbuffer_flatbuffer",
srcs = ["json_to_flatbuffer.fbs"],
gen_reflections = 1,
+ visibility = ["//aos:__subpackages__"],
)
cc_library(
diff --git a/aos/network/www/BUILD b/aos/network/www/BUILD
index 67e54f8..33291b2 100644
--- a/aos/network/www/BUILD
+++ b/aos/network/www/BUILD
@@ -1,5 +1,6 @@
load("@build_bazel_rules_typescript//:defs.bzl", "ts_library")
-load("@build_bazel_rules_nodejs//:defs.bzl", "rollup_bundle")
+load("@build_bazel_rules_nodejs//:defs.bzl", "rollup_bundle", "nodejs_binary")
+load("//aos:config.bzl", "aos_config")
filegroup(
name = "files",
@@ -56,3 +57,57 @@
cmd = "cp $(location @com_github_google_flatbuffers//:flatjs) $@",
visibility = ["//aos:__subpackages__"],
)
+
+ts_library(
+ name = "reflection_test_main",
+ srcs = [
+ "reflection_test_main.ts",
+ ],
+ deps = [
+ ":reflection_ts",
+ "//aos/network/www:proxy",
+ ],
+)
+
+ts_library(
+ name = "reflection_ts",
+ srcs = ["reflection.ts"],
+ deps =
+ [
+ "//aos:configuration_ts_fbs",
+ "//aos:json_to_flatbuffer_flatbuffer_ts",
+ ],
+)
+
+aos_config(
+ name = "test_config",
+ src = "test_config_file.json",
+ flatbuffers = [
+ "//aos:configuration_fbs",
+ "//aos:json_to_flatbuffer_flatbuffer",
+ ],
+ deps = [
+ "//aos/events:config",
+ ],
+)
+
+rollup_bundle(
+ name = "reflection_test_bundle",
+ entry_point = "aos/network/www/reflection_test_main",
+ deps = [
+ ":reflection_test_main",
+ ],
+)
+
+sh_binary(
+ name = "web_proxy_demo",
+ srcs = ["web_proxy_demo.sh"],
+ data = [
+ ":flatbuffers",
+ ":reflection_test.html",
+ ":reflection_test_bundle",
+ ":test_config.json",
+ "//aos/network:web_proxy_main",
+ "//y2020:config.json",
+ ],
+)
diff --git a/aos/network/www/reflection.ts b/aos/network/www/reflection.ts
new file mode 100644
index 0000000..5141790
--- /dev/null
+++ b/aos/network/www/reflection.ts
@@ -0,0 +1,417 @@
+// This library provides a few basic reflection utilities for Flatbuffers.
+// Currently, this only really supports the level of reflection that would
+// be necessary to convert a serialized flatbuffer to JSON using just a
+// reflection.Schema flatbuffer describing the type.
+// The current implementation is also not necessarily robust to invalidly
+// constructed flatbuffers.
+// See reflection_test_main.ts for sample usage.
+
+import {BaseType, Schema, Object, Field} from 'aos/configuration_generated';
+
+// Returns the size, in bytes, of the given type. For vectors/strings/etc.
+// returns the size of the offset.
+function typeSize(baseType: BaseType): number {
+ switch (baseType) {
+ case BaseType.None:
+ case BaseType.UType:
+ case BaseType.Bool:
+ case BaseType.Byte:
+ case BaseType.UByte:
+ return 1;
+ case BaseType.Short:
+ case BaseType.UShort:
+ return 2;
+ case BaseType.Int:
+ case BaseType.UInt:
+ return 4;
+ case BaseType.Long:
+ case BaseType.ULong:
+ return 8;
+ case BaseType.Float:
+ return 4;
+ case BaseType.Double:
+ return 8;
+ case BaseType.String:
+ case BaseType.Vector:
+ case BaseType.Obj:
+ case BaseType.Union:
+ case BaseType.Array:
+ return 4;
+ }
+}
+
+// Returns whether the given type is a scalar type.
+function isScalar(baseType: BaseType): boolean {
+ switch (baseType) {
+ case BaseType.UType:
+ case BaseType.Bool:
+ case BaseType.Byte:
+ case BaseType.UByte:
+ case BaseType.Short:
+ case BaseType.UShort:
+ case BaseType.Int:
+ case BaseType.UInt:
+ case BaseType.Long:
+ case BaseType.ULong:
+ case BaseType.Float:
+ case BaseType.Double:
+ return true;
+ case BaseType.None:
+ case BaseType.String:
+ case BaseType.Vector:
+ case BaseType.Obj:
+ case BaseType.Union:
+ case BaseType.Array:
+ return false;
+ }
+}
+
+// Returns whether the given type is integer or not.
+function isInteger(baseType: BaseType): boolean {
+ switch (baseType) {
+ case BaseType.UType:
+ case BaseType.Bool:
+ case BaseType.Byte:
+ case BaseType.UByte:
+ case BaseType.Short:
+ case BaseType.UShort:
+ case BaseType.Int:
+ case BaseType.UInt:
+ case BaseType.Long:
+ case BaseType.ULong:
+ return true;
+ case BaseType.Float:
+ case BaseType.Double:
+ case BaseType.None:
+ case BaseType.String:
+ case BaseType.Vector:
+ case BaseType.Obj:
+ case BaseType.Union:
+ case BaseType.Array:
+ return false;
+ }
+}
+
+// Returns whether the given type is a long--this is needed to know whether it
+// can be represented by the normal javascript number (8-byte integers require a
+// special type, since the native number type is an 8-byte double, which won't
+// represent 8-byte integers to full precision).
+function isLong(baseType: BaseType): boolean {
+ return isInteger(baseType) && (typeSize(baseType) > 4);
+}
+
+// TODO(james): Use the actual flatbuffers.ByteBuffer object; this is just
+// to prevent the typescript compiler from complaining.
+class ByteBuffer {}
+
+// Stores the data associated with a Table within a given buffer.
+export class Table {
+ // Wrapper to represent an object (Table or Struct) within a ByteBuffer.
+ // The ByteBuffer is the raw data associated with the object.
+ // typeIndex is an index into the schema object vector for the parser
+ // object that this is associated with.
+ // offset is the absolute location within the buffer where the root of the
+ // object is.
+ // Note that a given Table assumes that it is being used with a particular
+ // Schema object.
+ // External users should generally not be using this constructor directly.
+ constructor(
+ public readonly bb: ByteBuffer,
+ public readonly typeIndex: number, public readonly offset: number) {}
+ // Constructs a Table object for the root of a ByteBuffer--this assumes that
+ // the type of the Table is the root table of the Parser that you are using.
+ static getRootTable(bb: ByteBuffer): Table {
+ return new Table(bb, -1, bb.readInt32(bb.position()) + bb.position());
+ }
+ // Reads a scalar of a given type at a given offset.
+ readScalar(fieldType: BaseType, offset: number) {
+ switch (fieldType) {
+ case BaseType.UType:
+ case BaseType.Bool:
+ return this.bb.readUint8(offset);
+ case BaseType.Byte:
+ return this.bb.readInt8(offset);
+ case BaseType.UByte:
+ return this.bb.readUint8(offset);
+ case BaseType.Short:
+ return this.bb.readInt16(offset);
+ case BaseType.UShort:
+ return this.bb.readUint16(offset);
+ case BaseType.Int:
+ return this.bb.readInt32(offset);
+ case BaseType.UInt:
+ return this.bb.readUint32(offset);
+ case BaseType.Long:
+ return this.bb.readInt64(offset);
+ case BaseType.ULong:
+ return this.bb.readUint64(offset);
+ case BaseType.Float:
+ return this.bb.readFloat32(offset);
+ case BaseType.Double:
+ return this.bb.readFloat64(offset);
+ }
+ throw new Error('Unsupported message type ' + baseType);
+ }
+};
+
+// The Parser class uses a Schema to provide all the utilities required to
+// parse flatbuffers that have a type that is the same as the root_type defined
+// by the Schema.
+// The classical usage would be to, e.g., be reading a channel with a type of
+// "aos.FooBar". At startup, you would construct a Parser from the channel's
+// Schema. When a message is received on the channel , you would then use
+// Table.getRootTable() on the received buffer to construct the Table, and
+// then access the members using the various methods of the Parser (or just
+// convert the entire object to a javascript Object/JSON using toObject()).
+export class Parser {
+ constructor(private readonly schema: Schema) {}
+
+ // Parse a Table to a javascript object. This is can be used, e.g., to convert
+ // a flatbuffer Table to JSON.
+ // If readDefaults is set to true, then scalar fields will be filled out with
+ // their default values if not populated; if readDefaults is false and the
+ // field is not populated, the resulting object will not populate the field.
+ toObject(table: Table, readDefaults: boolean = false) {
+ const result = {};
+ const schema = this.getType(table.typeIndex);
+ const numFields = schema.fieldsLength();
+ for (let ii = 0; ii < numFields; ++ii) {
+ const field = schema.fields(ii);
+ const baseType = field.type().baseType();
+ let fieldValue = null;
+ if (isScalar(baseType)) {
+ fieldValue = this.readScalar(table, field.name(), readDefaults);
+ } else if (baseType === BaseType.String) {
+ fieldValue = this.readString(table, field.name());
+ } else if (baseType === BaseType.Obj) {
+ const subTable = this.readTable(table, field.name());
+ if (subTable !== null) {
+ fieldValue = this.toObject(subTable, readDefaults);
+ }
+ } else if (baseType === BaseType.Vector) {
+ const elementType = field.type().element();
+ if (isScalar(elementType)) {
+ fieldValue = this.readVectorOfScalars(table, field.name());
+ } else if (elementType === BaseType.String) {
+ fieldValue = this.readVectorOfStrings(table, field.name());
+ } else if (elementType === BaseType.Obj) {
+ const tables = this.readVectorOfTables(table, field.name());
+ if (tables !== null) {
+ fieldValue = [];
+ for (const table of tables) {
+ fieldValue.push(this.toObject(table, readDefaults));
+ }
+ }
+ } else {
+ throw new Error('Vectors of Unions and Arrays are not supported.');
+ }
+ } else {
+ throw new Error(
+ 'Unions and Arrays are not supported in field ' + field.name());
+ }
+ if (fieldValue !== null) {
+ result[field.name()] = fieldValue;
+ }
+ }
+ return result;
+ }
+
+ // Returns the Object definition associated with the given type index.
+ getType(typeIndex: number): Object {
+ if (typeIndex === -1) {
+ return this.schema.rootTable();
+ }
+ if (typeIndex < 0 || typeIndex > this.schema.objectsLength()) {
+ throw new Error("Type index out-of-range.");
+ }
+ return this.schema.objects(typeIndex);
+ }
+
+ // Retrieves the Field schema for the given field name within a given
+ // type index.
+ getField(fieldName: string, typeIndex: number): Field {
+ const schema: Object = this.getType(typeIndex);
+ const numFields = schema.fieldsLength();
+ for (let ii = 0; ii < numFields; ++ii) {
+ const field = schema.fields(ii);
+ const name = field.name();
+ if (fieldName === name) {
+ return field;
+ }
+ }
+ throw new Error(
+ 'Couldn\'t find field ' + fieldName + ' options are ' + fields);
+ }
+
+ // Reads a scalar with the given field name from a Table. If readDefaults
+ // is set to false and the field is unset, we will return null. If
+ // readDefaults is true and the field is unset, we will look-up the default
+ // value for the field and return that.
+ // For 64-bit fields, returns a flatbuffer Long rather than a standard number.
+ // TODO(james): For this and other accessors, determine if there is a
+ // significant performance gain to be had by using readScalar to construct
+ // an accessor method rather than having to redo the schema inspection on
+ // every call.
+ readScalar(table: Table, fieldName: string, readDefaults: boolean = false) {
+ const field = this.getField(fieldName, table.typeIndex);
+ const fieldType = field.type();
+ const isStruct = this.getType(table.typeIndex).isStruct();
+ if (!isScalar(fieldType.baseType())) {
+ throw new Error('Field ' + fieldName + ' is not a scalar type.');
+ }
+
+ if (isStruct) {
+ return table.readScalar(
+ fieldType.baseType(), table.offset + field.offset());
+ }
+
+ const offset =
+ table.offset + table.bb.__offset(table.offset, field.offset());
+ if (offset === table.offset) {
+ if (!readDefaults) {
+ return null;
+ }
+ if (isInteger(fieldType.baseType())) {
+ if (isLong(fieldType.baseType())) {
+ return field.defaultInteger();
+ } else {
+ if (field.defaultInteger().high != 0) {
+ throw new Error(
+ '<=4 byte integer types should not use 64-bit default values.');
+ }
+ return field.defaultInteger().low;
+ }
+ } else {
+ return field.defaultReal();
+ }
+ }
+ return table.readScalar(fieldType.baseType(), offset);
+ }
+ // Reads a string with the given field name from the provided Table.
+ // If the field is unset, returns null.
+ readString(table: Table, fieldName: string): string|null {
+ const field = this.getField(fieldName, table.typeIndex);
+ const fieldType = field.type();
+ if (fieldType.baseType() !== BaseType.String) {
+ throw new Error('Field ' + fieldName + ' is not a string.');
+ }
+
+ const offsetToOffset =
+ table.offset + table.bb.__offset(table.offset, field.offset());
+ if (offsetToOffset === table.offset) {
+ return null;
+ }
+ return table.bb.__string(offsetToOffset);
+ }
+ // Reads a sub-message from the given Table. The sub-message may either be
+ // a struct or a Table. Returns null if the sub-message is not set.
+ readTable(table: Table, fieldName: string): Table|null {
+ const field = this.getField(fieldName, table.typeIndex);
+ const fieldType = field.type();
+ const parentIsStruct = this.getType(table.typeIndex).isStruct();
+ if (fieldType.baseType() !== BaseType.Obj) {
+ throw new Error('Field ' + fieldName + ' is not an object type.');
+ }
+
+ if (parentIsStruct) {
+ return new Table(
+ table.bb, fieldType.index(), table.offset + field.offset());
+ }
+
+ const offsetToOffset =
+ table.offset + table.bb.__offset(table.offset, field.offset());
+ if (offsetToOffset === table.offset) {
+ return null;
+ }
+
+ const elementIsStruct = this.getType(fieldType.index()).isStruct();
+
+ const objectStart =
+ elementIsStruct ? offsetToOffset : table.bb.__indirect(offsetToOffset);
+ return new Table(table.bb, fieldType.index(), objectStart);
+ }
+ // Reads a vector of scalars (like readScalar, may return a vector of Long's
+ // instead). Also, will return null if the vector is not set.
+ readVectorOfScalars(table: Table, fieldName: string): number[]|null {
+ const field = this.getField(fieldName, table.typeIndex);
+ const fieldType = field.type();
+ if (fieldType.baseType() !== BaseType.Vector) {
+ throw new Error('Field ' + fieldName + ' is not an vector.');
+ }
+ if (!isScalar(fieldType.element())) {
+ throw new Error('Field ' + fieldName + ' is not an vector of scalars.');
+ }
+
+ const offsetToOffset =
+ table.offset + table.bb.__offset(table.offset, field.offset());
+ if (offsetToOffset === table.offset) {
+ return null;
+ }
+ const numElements = table.bb.__vector_len(offsetToOffset);
+ const result = [];
+ const baseOffset = table.bb.__vector(offsetToOffset);
+ const scalarSize = typeSize(fieldType.element());
+ for (let ii = 0; ii < numElements; ++ii) {
+ result.push(
+ table.readScalar(fieldType.element(), baseOffset + scalarSize * ii));
+ }
+ return result;
+ }
+ // Reads a vector of tables. Returns null if vector is not set.
+ readVectorOfTables(table: Table, fieldName: string) {
+ const field = this.getField(fieldName, table.typeIndex);
+ const fieldType = field.type();
+ if (fieldType.baseType() !== BaseType.Vector) {
+ throw new Error('Field ' + fieldName + ' is not an vector.');
+ }
+ if (fieldType.element() !== BaseType.Obj) {
+ throw new Error('Field ' + fieldName + ' is not an vector of objects.');
+ }
+
+ const offsetToOffset =
+ table.offset + table.bb.__offset(table.offset, field.offset());
+ if (offsetToOffset === table.offset) {
+ return null;
+ }
+ const numElements = table.bb.__vector_len(offsetToOffset);
+ const result = [];
+ const baseOffset = table.bb.__vector(offsetToOffset);
+ const elementSchema = this.getType(fieldType.index());
+ const elementIsStruct = elementSchema.isStruct();
+ const elementSize = elementIsStruct ? elementSchema.bytesize() :
+ typeSize(fieldType.element());
+ for (let ii = 0; ii < numElements; ++ii) {
+ const elementOffset = baseOffset + elementSize * ii;
+ result.push(new Table(
+ table.bb, fieldType.index(),
+ elementIsStruct ? elementOffset :
+ table.bb.__indirect(elementOffset)));
+ }
+ return result;
+ }
+ // Reads a vector of strings. Returns null if not set.
+ readVectorOfStrings(table: Table, fieldName: string): string[]|null {
+ const field = this.getField(fieldName, table.typeIndex);
+ const fieldType = field.type();
+ if (fieldType.baseType() !== BaseType.Vector) {
+ throw new Error('Field ' + fieldName + ' is not an vector.');
+ }
+ if (fieldType.element() !== BaseType.String) {
+ throw new Error('Field ' + fieldName + ' is not an vector of strings.');
+ }
+
+ const offsetToOffset =
+ table.offset + table.bb.__offset(table.offset, field.offset());
+ if (offsetToOffset === table.offset) {
+ return null;
+ }
+ const numElements = table.bb.__vector_len(offsetToOffset);
+ const result = [];
+ const baseOffset = table.bb.__vector(offsetToOffset);
+ const offsetSize = typeSize(fieldType.element());
+ for (let ii = 0; ii < numElements; ++ii) {
+ result.push(table.bb.__string(baseOffset + offsetSize * ii));
+ }
+ return result;
+ }
+}
diff --git a/aos/network/www/reflection_test.html b/aos/network/www/reflection_test.html
new file mode 100644
index 0000000..b9cc6c8
--- /dev/null
+++ b/aos/network/www/reflection_test.html
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <script src="flatbuffers.js"></script>
+ <script src="reflection_test_bundle.min.js" defer></script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/aos/network/www/reflection_test_main.ts b/aos/network/www/reflection_test_main.ts
new file mode 100644
index 0000000..5c6436e
--- /dev/null
+++ b/aos/network/www/reflection_test_main.ts
@@ -0,0 +1,159 @@
+import {Configuration, Schema} from 'aos/configuration_generated'
+import {BaseType,
+ Configuration as TestTable,
+ FooStruct,
+ FooStructNested,
+ Location,
+ Map,
+ VectorOfStrings,
+ VectorOfVectorOfString} from 'aos/json_to_flatbuffer_generated'
+
+import {Connection} from './proxy';
+import {Parser, Table} from './reflection'
+// This file runs a basic test to confirm that the typescript flatbuffer
+// reflection library is working correctly. It currently is not run
+// automatically--to run it, run the web_proxy_demo sh_binary target, open the
+// resulting reflection_test.html webpage, open the console and confirm that
+// "TEST PASSED" has been printed.
+
+const conn = new Connection();
+
+conn.connect();
+
+function assertEqual(a: any, b: any, msg?: string): void {
+ if (a !== b) {
+ throw new Error(a + ' !== ' + b + ': ' + msg);
+ }
+}
+
+// Constructs a flatbuffer and then uses Parser.toObject to parse it and confirm
+// that the start/end results are the same. This is largely meant to ensure
+// that we are exercising most of the logic associated with parsing flatbuffers.
+function DoTest(config: Configuration): void {
+ const builder = new flatbuffers.Builder();
+ {
+ TestTable.startVectorFooStructVector(builder, 3);
+ const fooStruct0 = FooStruct.createFooStruct(builder, 66, 118);
+ const fooStruct1 = FooStruct.createFooStruct(builder, 67, 118);
+ const fooStruct2 = FooStruct.createFooStruct(builder, 68, 118);
+ const vectorFooStruct = builder.endVector();
+ const nameString = builder.createString('nameString');
+ const typeString = builder.createString('typeString');
+ const location0 =
+ Location.createLocation(builder, nameString, typeString, 100, 200);
+ const location1 =
+ Location.createLocation(builder, nameString, typeString, 300, 400);
+ const map = Map.createMap(builder, location0, location1);
+ const mapVector = TestTable.createMapsVector(builder, [map]);
+
+ const strVector =
+ VectorOfStrings.createStrVector(builder, [nameString, typeString]);
+ const vectorOfStrings =
+ VectorOfStrings.createVectorOfStrings(builder, strVector);
+ const vVector =
+ VectorOfVectorOfString.createVVector(builder, [vectorOfStrings]);
+ const vectorOfVectorOfStrings =
+ VectorOfVectorOfString.createVectorOfVectorOfString(builder, vVector);
+
+ const doubleVector =
+ TestTable.createVectorFooDoubleVector(builder, [9.71, 1.678, 2.056]);
+
+ TestTable.startConfiguration(builder);
+ TestTable.addMaps(builder, mapVector);
+ TestTable.addVov(builder, vectorOfVectorOfStrings);
+ const fooStruct = FooStruct.createFooStruct(builder, 33, 118);
+ TestTable.addFooStruct(builder, fooStruct);
+ TestTable.addVectorFooStruct(builder, vectorFooStruct);
+ TestTable.addVectorFooDouble(builder, doubleVector);
+ TestTable.addFooDouble(builder, 11.14);
+ TestTable.addFooLong(builder, new flatbuffers.Long(100, 1));
+ TestTable.addFooEnum(builder, BaseType.Array);
+ }
+
+ builder.finish(Configuration.endConfiguration(builder));
+ const array = builder.asUint8Array();
+ const fbBuffer = new flatbuffers.ByteBuffer(array);
+
+ const parsedFb = TestTable.getRootAsConfiguration(fbBuffer);
+
+ let testSchema = null;
+ for (let ii = 0; ii < config.channelsLength(); ++ii) {
+ if (config.channels(ii).type() === 'aos.testing.Configuration') {
+ testSchema = config.channels(ii).schema();
+ }
+ }
+ if (testSchema === null) {
+ throw new Error('Couldn\'t find test schema in config.');
+ }
+ const testParser = new Parser(testSchema);
+ const testTable = Table.getRootTable(fbBuffer);
+ const testObject = testParser.toObject(testTable, false);
+
+ console.log('Parsed test object:');
+ console.log(testObject);
+
+ assertEqual(11.14, parsedFb.fooDouble());
+ assertEqual(testObject['foo_double'], parsedFb.fooDouble());
+ assertEqual(testObject['foo_enum'], parsedFb.fooEnum());
+ assertEqual(testObject['foo_long'].low, parsedFb.fooLong().low);
+ assertEqual(testObject['foo_long'].high, parsedFb.fooLong().high);
+ assertEqual(testObject['foo_ulong'], undefined);
+ assertEqual(testObject['locations'], undefined);
+
+ const maps = testObject['maps'];
+ assertEqual(maps.length, 1);
+ assertEqual(maps[0]['match']['name'], 'nameString');
+ assertEqual(maps[0]['rename']['name'], 'nameString');
+ assertEqual(maps[0]['match']['type'], 'typeString');
+ assertEqual(maps[0]['rename']['type'], 'typeString');
+ assertEqual(
+ maps[0]['match']['frequency'], parsedFb.maps(0).match().frequency());
+ assertEqual(maps[0]['rename']['frequency'], 300);
+ assertEqual(maps[0]['match']['max_size'], 200);
+ assertEqual(maps[0]['rename']['max_size'], 400);
+
+ assertEqual(
+ testObject['foo_struct']['foo_byte'], parsedFb.fooStruct().fooByte());
+ assertEqual(
+ testObject['foo_struct']['nested_struct']['foo_byte'],
+ parsedFb.fooStruct().nestedStruct().fooByte());
+
+ const fooStructs = testObject['vector_foo_struct'];
+ assertEqual(fooStructs.length, 3);
+ for (let ii = 0; ii < 3; ++ii) {
+ assertEqual(
+ fooStructs[ii]['foo_byte'], parsedFb.vectorFooStruct(ii).fooByte());
+ assertEqual(
+ fooStructs[ii]['nested_struct']['foo_byte'],
+ parsedFb.vectorFooStruct(ii).nestedStruct().fooByte());
+ }
+
+ for (let ii = 0; ii < 3; ++ii) {
+ assertEqual(
+ testObject['vector_foo_double'][ii], parsedFb.vectorFooDouble(ii));
+ }
+
+ assertEqual(testObject['vov']['v'].length, 1);
+ assertEqual(testObject['vov']['v'][0]['str'].length, 2);
+ assertEqual(testObject['vov']['v'][0]['str'][0], parsedFb.vov().v(0).str(0));
+ assertEqual(testObject['vov']['v'][0]['str'][1], parsedFb.vov().v(0).str(1));
+ console.log('TEST PASSED');
+}
+
+conn.addConfigHandler((config: Configuration) => {
+ let configSchema = null;
+ for (let ii = 0; ii < config.channelsLength(); ++ii) {
+ if (config.channels(ii).type() === 'aos.Configuration') {
+ configSchema = config.channels(ii).schema();
+ }
+ }
+ if (configSchema === null) {
+ throw new Error('Couldn\'t find Configuration schema in config.');
+ }
+ let configParser = new Parser(configSchema);
+ const configTable = Table.getRootTable(config.bb);
+ console.log('Received config:');
+ console.log(configParser.toObject(configTable, true));
+
+ DoTest(config);
+});
diff --git a/aos/network/www/test_config_file.json b/aos/network/www/test_config_file.json
new file mode 100644
index 0000000..09b22dd
--- /dev/null
+++ b/aos/network/www/test_config_file.json
@@ -0,0 +1,21 @@
+{
+ "channels": [
+ {
+ "name": "/test",
+ "type": "aos.testing.Configuration",
+ "source_node": "roborio",
+ "frequency": 200
+ },
+ {
+ "name": "/test",
+ "type": "aos.Configuration",
+ "source_node": "roborio",
+ "max_size": 1678,
+ "frequency": 200
+ }
+ ],
+ "channel_storage_duration": 31415,
+ "imports": [
+ "../../../aos/events/aos.json"
+ ]
+}
diff --git a/aos/network/www/web_proxy_demo.sh b/aos/network/www/web_proxy_demo.sh
new file mode 100755
index 0000000..c3fe715
--- /dev/null
+++ b/aos/network/www/web_proxy_demo.sh
@@ -0,0 +1 @@
+./aos/network/web_proxy_main --config=aos/network/www/test_config.json --data_dir=aos/network/www