-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds OMG IDL definitions for Foxglove schemas.
- Loading branch information
Showing
58 changed files
with
1,574 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"`, | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.