Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate .idl schemas #126

Merged
merged 6 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
jtbandes marked this conversation as resolved.
Show resolved Hide resolved

// 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
Loading