import { FoxgloveEnumSchema, FoxgloveMessageField, FoxgloveMessageSchema, FoxglovePrimitive, } from "./types"; function primitiveToProto(type: Exclude) { switch (type) { case "uint32": return "fixed32"; case "bytes": return "bytes"; case "string": return "string"; case "boolean": return "bool"; case "float64": return "double"; } } export function generateProto( schema: FoxgloveMessageSchema, nestedEnums: FoxgloveEnumSchema[], ): string { const enumDefinitions: string[] = []; for (const enumSchema of nestedEnums) { const fields = enumSchema.values.map(({ name, value, description }) => { if (description != undefined) { return `// ${description}\n ${name} = ${value};`; } else { return `${name} = ${value};`; } }); enumDefinitions.push( ` // ${enumSchema.description}\n enum ${enumSchema.protobufEnumName} {\n ${fields.join( "\n\n ", )}\n }\n`, ); } const explicitFieldNumbers = new Set(); for (const field of schema.fields) { if (field.protobufFieldNumber != undefined) { if (explicitFieldNumbers.has(field.protobufFieldNumber)) { throw new Error( `More than one field with protobufFieldNumber ${field.protobufFieldNumber}`, ); } explicitFieldNumbers.add(field.protobufFieldNumber); } } let nextFieldNumber = 1; const numberedFields = schema.fields.map( (field): FoxgloveMessageField & { protobufFieldNumber: number } => { if (field.protobufFieldNumber != undefined) { return { ...field, protobufFieldNumber: field.protobufFieldNumber }; } while (explicitFieldNumbers.has(nextFieldNumber)) { ++nextFieldNumber; } return { ...field, protobufFieldNumber: nextFieldNumber++ }; }, ); const imports = new Set(); const fields = numberedFields.map((field) => { const lineComments: string[] = []; const qualifiers: string[] = []; if (field.array != undefined) { qualifiers.push("repeated"); } if (typeof field.array === "number") { lineComments.push(`length ${field.array}`); } switch (field.type.type) { case "enum": qualifiers.push(field.type.enum.protobufEnumName); break; case "nested": qualifiers.push(`foxglove.${field.type.schema.name}`); imports.add(`foxglove/${field.type.schema.name}`); break; case "primitive": if (field.type.name === "time") { qualifiers.push("google.protobuf.Timestamp"); imports.add(`google/protobuf/timestamp`); } else if (field.type.name === "duration") { qualifiers.push("google.protobuf.Duration"); imports.add(`google/protobuf/duration`); } else { qualifiers.push(primitiveToProto(field.type.name)); } break; } return `${field.description .trim() .split("\n") .map((line) => ` // ${line}\n`) .join("")} ${qualifiers.join(" ")} ${field.name} = ${field.protobufFieldNumber};${ lineComments.length > 0 ? " // " + lineComments.join(", ") : "" }`; }); const definition = `// ${schema.description}\nmessage ${schema.name} {\n${enumDefinitions.join( "\n\n", )}${fields.join("\n\n")}\n}`; const outputSections = [ `// Generated by https://github.com/foxglove/schemas`, 'syntax = "proto3";', Array.from(imports) .sort() .map((name) => `import "${name}.proto";`) .join("\n"), `package foxglove;`, definition, ].filter(Boolean); return outputSections.join("\n\n") + "\n"; }