Skip to content

Commit

Permalink
Generate .idl schemas (#126)
Browse files Browse the repository at this point in the history
Adds OMG IDL definitions for Foxglove schemas.
  • Loading branch information
jtbandes authored Aug 28, 2023
1 parent e243fee commit 63f59f5
Show file tree
Hide file tree
Showing 58 changed files with 1,574 additions and 37 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Message schemas supported by [Foxglove Studio](https://studio.foxglove.dev)

See [Foxglove Schemas documentation](https://foxglove.dev/docs/studio/messages).

The [schemas](./schemas) folder contains type definitions generated for ROS 1, ROS 2, Protobuf, JSON Schema, and TypeScript.
The [schemas](./schemas) folder contains type definitions generated for ROS 1, ROS 2, Protobuf, JSON Schema, TypeScript, and OMG IDL.

These schemas can be used in [MCAP](https://github.com/foxglove/mcap) files or [Foxglove WebSocket](https://github.com/foxglove/ws-protocol) servers to take advantage of Foxglove Studio's visualizations.

Expand Down
4 changes: 2 additions & 2 deletions internal/generateFlatbufferSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ describe("generateFlatbuffers", () => {
/// An example enum
enum ExampleEnum : ubyte {
/// Value A
A = 1,
A = 0,
/// Value B
B = 2,
B = 1,
}
/// An example type
table ExampleMessage {
Expand Down
8 changes: 4 additions & 4 deletions internal/generateJsonSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,12 @@ describe("generateJsonSchema", () => {
"description": "An enum field",
"oneOf": [
{
"const": 1,
"const": 0,
"description": "Value A",
"title": "A",
},
{
"const": 2,
"const": 1,
"description": "Value B",
"title": "B",
},
Expand All @@ -129,12 +129,12 @@ describe("generateJsonSchema", () => {
"description": "An enum array field",
"oneOf": [
{
"const": 1,
"const": 0,
"description": "Value A",
"title": "A",
},
{
"const": 2,
"const": 1,
"description": "Value B",
"title": "B",
},
Expand Down
187 changes: 187 additions & 0 deletions internal/generateOmgIdl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { parseIdl } from "@foxglove/omgidl-parser";

import { DURATION_IDL, TIME_IDL, generateOmgIdl } from "./generateOmgIdl";
import { foxgloveEnumSchemas, foxgloveMessageSchemas } from "./schemas";
import { exampleEnum, exampleMessage } from "./testFixtures";

describe("generateOmgIdl", () => {
it("generates .idl files", () => {
expect(generateOmgIdl(exampleEnum)).toMatchInlineSnapshot(`
"// Generated by https://github.com/foxglove/schemas
module foxglove {
// An example enum
enum ExampleEnum {
// Value A
// Value: 0
A,
// Value B
// Value: 1
B
};
};
"
`);
expect(generateOmgIdl(exampleMessage)).toMatchInlineSnapshot(`
"// Generated by https://github.com/foxglove/schemas
#include "foxglove/Duration.idl"
#include "foxglove/ExampleEnum.idl"
#include "foxglove/NestedMessage.idl"
#include "foxglove/Time.idl"
module foxglove {
// An example type
struct ExampleMessage {
// duration field
Duration field_duration;
// time field
Time field_time;
// boolean field
@default(TRUE)
boolean field_boolean;
// bytes field
sequence<uint8> field_bytes;
// float64 field
@default(1.0)
double field_float64;
// uint32 field
@default(5)
uint32 field_uint32;
// string field
@default("string-type")
string field_string;
// duration array field
sequence<Duration> field_duration_array;
// time array field
sequence<Time> field_time_array;
// boolean array field
sequence<boolean> field_boolean_array;
// bytes array field
sequence<sequence<uint8>> field_bytes_array;
// float64 array field
sequence<double> field_float64_array;
// uint32 array field
sequence<uint32> field_uint32_array;
// string array field
sequence<string> field_string_array;
// duration fixed-length array field
Duration field_duration_fixed_array[3];
// time fixed-length array field
Time field_time_fixed_array[3];
// boolean fixed-length array field
boolean field_boolean_fixed_array[3];
// bytes fixed-length array field
sequence<uint8> field_bytes_fixed_array[3];
// float64 fixed-length array field
double field_float64_fixed_array[3];
// uint32 fixed-length array field
uint32 field_uint32_fixed_array[3];
// string fixed-length array field
string field_string_fixed_array[3];
// An enum field
ExampleEnum field_enum;
// An enum array field
sequence<ExampleEnum> field_enum_array;
// A nested field
NestedMessage field_nested;
// A nested array field
// With
// a
// very
// long
// description
sequence<NestedMessage> field_nested_array;
};
};
"
`);
});

const allIdlFiles = new Map<string, string>([
["Time", TIME_IDL],
["Duration", DURATION_IDL],
...Object.entries(foxgloveMessageSchemas).map(([name, schema]): [string, string] => [
name,
generateOmgIdl(schema),
]),
...Object.entries(foxgloveEnumSchemas).map(([name, schema]): [string, string] => [
name,
generateOmgIdl(schema),
]),
]);

it.each(Object.values(foxgloveMessageSchemas))("generates parseable .idl for $name", (schema) => {
const includePattern = /^#include "foxglove\/(.*)\.idl"$/gm;
let idl = generateOmgIdl(schema);
while (includePattern.test(idl)) {
idl = idl.replace(includePattern, (_match, name: string) => {
const file = allIdlFiles.get(name);
if (file == undefined) {
throw new Error(`Invalid include ${name}`);
}
return file;
});
}
expect(() => parseIdl(idl)).not.toThrow();
});

it("refuses to generate enum with non-sequential values", () => {
expect(() =>
generateOmgIdl({
type: "enum",
name: "Foo",
description: "",
parentSchemaName: "Bar",
protobufEnumName: "Foo",
values: [{ name: "A", value: 1 }],
}),
).toThrowErrorMatchingInlineSnapshot(
`"Enum value Foo.A at index 0 has value 1; index and value must match for OMG IDL"`,
);
expect(() =>
generateOmgIdl({
type: "enum",
name: "Foo",
description: "",
parentSchemaName: "Bar",
protobufEnumName: "Foo",
values: [
{ name: "A", value: 0 },
{ name: "B", value: 3 },
],
}),
).toThrowErrorMatchingInlineSnapshot(
`"Enum value Foo.B at index 1 has value 3; index and value must match for OMG IDL"`,
);
});
});
141 changes: 141 additions & 0 deletions internal/generateOmgIdl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { FoxglovePrimitive, FoxgloveSchema } from "./types";

function primitiveToIdl(type: Exclude<FoxglovePrimitive, "time" | "duration">) {
switch (type) {
case "bytes":
return "sequence<uint8>";
case "string":
return "string";
case "boolean":
return "boolean";
case "float64":
return "double";
case "uint32":
return "uint32";
}
}

export const TIME_IDL = `\
module foxglove {
struct Time {
uint32 sec;
uint32 nsec;
};
};
`;

export const DURATION_IDL = `\
module foxglove {
struct Duration {
int32 sec;
uint32 nsec;
};
};
`;

export function generateOmgIdl(schema: FoxgloveSchema): string {
const imports = new Set<string>();

let definition: string;
switch (schema.type) {
case "enum": {
const fields = schema.values.map(({ name, value, description }, index) => {
const separator = index === schema.values.length - 1 ? "" : ",";
if (value !== index) {
throw new Error(
`Enum value ${schema.name}.${name} at index ${index} has value ${value}; index and value must match for OMG IDL`,
);
}
if (description != undefined) {
return `// ${description}\n // Value: ${value}\n ${name}${separator}`;
} else {
return `// Value: ${value}\n ${name}${separator}`;
}
});
definition = `// ${schema.description}\nenum ${schema.name} {\n ${fields.join(
"\n\n ",
)}\n};`;
break;
}

case "message": {
const fields = schema.fields.map((field) => {
let fieldType: string;
switch (field.type.type) {
case "enum":
fieldType = field.type.enum.name;
imports.add(field.type.enum.name);
break;
case "nested":
fieldType = field.type.schema.name;
imports.add(field.type.schema.name);
break;
case "primitive":
if (field.type.name === "time") {
fieldType = "Time";
imports.add("Time");
} else if (field.type.name === "duration") {
fieldType = "Duration";
imports.add("Duration");
} else {
fieldType = primitiveToIdl(field.type.name);
}
break;
}
let arraySize = "";
if (typeof field.array === "number") {
arraySize = `[${field.array}]`;
} else if (field.array != undefined) {
fieldType = `sequence<${fieldType}>`;
}
const descriptionLines = field.description.trim().split("\n");
const comment = descriptionLines.map((line) => `// ${line}`).join("\n ");

let defaultAnnotation = "";
if (typeof field.defaultValue === "string") {
defaultAnnotation = `@default(${JSON.stringify(field.defaultValue)})\n `;
} else if (typeof field.defaultValue === "number") {
// For floating-point fields with integer default values, ensure we output a number with
// at least one decimal place so it is interpreted as an IDL floating-point literal
if (
field.type.type === "primitive" &&
field.type.name === "float64" &&
Number.isInteger(field.defaultValue)
) {
defaultAnnotation = `@default(${field.defaultValue.toFixed(1)})\n `;
} else {
defaultAnnotation = `@default(${field.defaultValue})\n `;
}
} else if (typeof field.defaultValue === "boolean") {
defaultAnnotation = `@default(${field.defaultValue ? "TRUE" : "FALSE"})\n `;
}

return `${comment}\n ${defaultAnnotation}${fieldType} ${field.name}${arraySize};`;
});

definition = `// ${schema.description}\nstruct ${schema.name} {\n ${fields.join(
"\n\n ",
)}\n};`;
break;
}
}

const outputSections = [
`// Generated by https://github.com/foxglove/schemas`,

Array.from(imports)
.sort()
.map((name) => `#include "foxglove/${name}.idl"`)
.join("\n"),

"module foxglove {",
definition,
"};",
].filter(Boolean);

return outputSections.join("\n\n") + "\n";
}
4 changes: 2 additions & 2 deletions internal/generateProto.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ describe("generateProto", () => {
// An example enum
enum ExampleProtoEnum {
// Value A
A = 1;
A = 0;
// Value B
B = 2;
B = 1;
}
// duration field
google.protobuf.Duration field_duration = 1;
Expand Down
Loading

0 comments on commit 63f59f5

Please sign in to comment.