Skip to content

Commit

Permalink
better typescript typings (#551)
Browse files Browse the repository at this point in the history
This commit makes the changes below:

* generate type class strings for messages and services
* narrow down `TypeClass` type from `string` to known values
* generate typings for *Wrapper javascript classes
* generate constants as 'const enum'

These provide intellisense for typeclass names and allow constants to be referenced by value.

The constants are generated with '_Constants' suffix, since ros does not allow underscores in
the message name, this should be safe from any conflicts. This is also same as how the ros idl
generator exports the constants.

Fix #550
  • Loading branch information
Teo Koon Peng authored Feb 17, 2020
1 parent db2a685 commit a04c6d2
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 116 deletions.
153 changes: 102 additions & 51 deletions rostsd_gen/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function generateAll() {
// load pkg and interface info (msgs and srvs)
const generatedPath = path.join(__dirname, '../generated/');
const pkgInfos = getPkgInfos(generatedPath);

// write message.d.ts file
const messagesFilePath = path.join(__dirname, '../types/interfaces.d.ts');
const fd = fs.openSync(messagesFilePath, 'w');
Expand All @@ -56,17 +56,19 @@ function getPkgInfos(generatedRoot) {

const pkgInfo = {
name: pkg,
messages: []
messages: [],
services: []
};

const pkgPath = path.join(rootDir, pkg);
const files = fs.readdirSync(pkgPath);
const files = fs.readdirSync(pkgPath).filter(fn => fn.endsWith('.js'));

for (let filename of files) {
const typeClass = fileName2Typeclass(filename);

if (typeClass.type === 'srv') { // skip __srv__<action>
if (!typeClass.name.endsWith('Request') && !typeClass.name.endsWith('Response')) {
pkgInfo.services.push(typeClass);
continue;
}
}
Expand All @@ -93,10 +95,12 @@ function getPkgInfos(generatedRoot) {


function savePkgInfoAsTSD(pkgInfos, fd) {

let fullMessageNames = ['string'];
let messagesMap = {
string: 'string',
};

fs.writeSync(fd, '/* eslint-disable camelcase */\n');
fs.writeSync(fd, '/* eslint-disable max-len */\n');
fs.writeSync(fd, '// DO NOT EDIT\n');
fs.writeSync(fd, '// This file is generated by the rostsd_gen script\n\n');

Expand All @@ -115,7 +119,7 @@ function savePkgInfoAsTSD(pkgInfos, fd) {
for (const msgInfo of pkgInfo.messages) {

if (msgInfo.typeClass.type != curNS) {
if (curNS) { // close current ns
if (curNS) { // close current ns
fs.writeSync(fd, ' }\n');
}

Expand All @@ -129,10 +133,11 @@ function savePkgInfoAsTSD(pkgInfos, fd) {
}

saveMsgInfoAsTSD(msgInfo, fd);
saveMsgWrapperAsTSD(msgInfo, fd);

// full path to this msg
const fullMessageName = `${pkgInfo.name}.${msgInfo.typeClass.type}.${msgInfo.typeClass.name}`;
fullMessageNames.push(fullMessageName);
const fullMessageName = `${pkgInfo.name}/${msgInfo.typeClass.type}/${msgInfo.typeClass.name}`;
messagesMap[fullMessageName] = `${pkgInfo.name}.${msgInfo.typeClass.type}.${msgInfo.typeClass.name}`;
}

if (curNS) {
Expand All @@ -144,66 +149,112 @@ function savePkgInfoAsTSD(pkgInfos, fd) {
fs.writeSync(fd, ' }\n\n');
}

// write type alias for Message
// e.g. type Message =
// string |
// std_msgs.msg.Bool |
// std_msgs.msg.Byte |
// ...
fs.writeSync(fd, ' type Message = \n');
for (let i=0; i < fullMessageNames.length; i++) {
fs.writeSync(fd, ' ' + fullMessageNames[i]);
if (i != fullMessageNames.length-1) {
fs.writeSync(fd, ' |\n');
}
// write messages type mappings
fs.writeSync(fd, ' type MessagesMap = {\n');
for (const key in messagesMap) {
fs.writeSync(fd, ` '${key}': ${messagesMap[key]},\n`);
}
fs.writeSync(fd, ' };\n');
fs.writeSync(fd, ' type MessageTypeClassName = keyof MessagesMap;\n');
fs.writeSync(fd, ' type Message = MessagesMap[MessageTypeClassName];\n');
fs.writeSync(fd, ' type MessageType<T> = T extends MessageTypeClassName ? MessagesMap[T] : object;\n\n');

// write message wrappers mappings
fs.writeSync(fd, ' type MessageTypeClassWrappersMap = {\n');
for (const key in messagesMap) {
if (key === 'string') {
fs.writeSync(fd, " 'string': never,\n");
continue;
}
fs.writeSync(fd, ` '${key}': ${messagesMap[key]}_WrapperType,\n`);
}
fs.writeSync(fd, ' };\n');
fs.writeSync(fd, ' type MessageWrapperType<T> = T extends MessageTypeClassName ? MessageTypeClassWrappersMap[T] : object;\n\n');

// write service type class string
const services = [];
for (const pkg of pkgInfos) {
services.push(...pkg.services);
}
if (!services.length) {
fs.writeSync(fd, ' type ServiceTypeClassName = never;\n\n');
} else {
fs.writeSync(fd, ' type ServiceTypeClassName = \n');
for (let i = 0; i < services.length; i++) {
const srv = services[i];
const srvTypeClassStr = `${srv.package}/${srv.type}/${srv.name}`;
fs.writeSync(fd, ` '${srvTypeClassStr}'`);

if (i !== services.length - 1) {
fs.writeSync(fd, ' |\n');
}
}
fs.writeSync(fd, ';\n\n');
}
fs.writeSync(fd, ';\n');

fs.writeSync(fd, ' type TypeClassName = MessageTypeClassName | ServiceTypeClassName;\n');

// close module declare
fs.writeSync(fd, '}\n');

fs.closeSync(fd);
}


function saveMsgInfoAsTSD(msgInfo, fd) {

// write type = xxxx {
const typeTemplate =
` export type ${msgInfo.typeClass.name} = {\n`;

fs.writeSync(fd, typeTemplate);

// write constant definitions
for (let i = 0; i < msgInfo.def.constants.length; i++) {
const constant = msgInfo.def.constants[i];
function saveMsgWrapperAsTSD(msgInfo, fd) {
const msgName = msgInfo.typeClass.name;
fs.writeSync(fd, ` export type ${msgName}_WrapperType = {\n`);
for (const constant of msgInfo.def.constants) {
const constantType = primitiveType2JSName(constant.type);
const tmpl = (constantType == 'string') ?
` ${constant.name}: '${constant.value}'` :
` ${constant.name}: ${constant.value}`;
fs.writeSync(fd, tmpl);

if (i != msgInfo.def.constants.length - 1) {
fs.writeSync(fd, ',\n');
} else if (msgInfo.def.fields.length > 0) {
fs.writeSync(fd, ',\n');
}
fs.writeSync(fd, ` readonly ${constant.name}: ${constantType},\n`);
}
fs.writeSync(fd, ` new(other?: ${msgName}): ${msgName},\n`);
fs.writeSync(fd, ' }\n');
}

// write field definitions

/**
* Writes the message fields as typescript definitions.
*
* @param {*} msgInfo ros message info
* @param {*} fd file descriptor
* @param {string} indent The amount of indent, in spaces
* @param {string} lineEnd The character to put at the end of each line, usually ','
* or ';'
* @param {string} typePrefix The prefix to put before the type name for
* non-primitive types
* @returns {undefined}
*/
function saveMsgFieldsAsTSD(msgInfo, fd, indent=0, lineEnd=',', typePrefix='') {
const indentStr = ' '.repeat(indent);
for (let i = 0; i < msgInfo.def.fields.length; i++) {
const field = msgInfo.def.fields[i];
const fieldType = fieldType2JSName(field);
const tmpl = ` ${field.name}: ${fieldType}`;
let fieldType = fieldType2JSName(field);
let tp = field.type.isPrimitiveType ? '' : typePrefix;
if (typePrefix === 'rclnodejs.') {
fieldType = 'any';
tp = '';
}
const tmpl = `${indentStr}${field.name}: ${tp}${fieldType}`;
fs.writeSync(fd, tmpl);
if (field.type.isArray) {
fs.writeSync(fd, '[]');
}
if (i != msgInfo.def.fields.length - 1) {
fs.writeSync(fd, ',');
}
fs.writeSync(fd, lineEnd);
fs.writeSync(fd, '\n');
}
}


function saveMsgInfoAsTSD(msgInfo, fd) {
// write type = xxxx {
const typeTemplate =
` export type ${msgInfo.typeClass.name} = {\n`;

fs.writeSync(fd, typeTemplate);

// write field definitions
saveMsgFieldsAsTSD(msgInfo, fd, 8);

// end of def
fs.writeSync(fd, ' };\n');
Expand All @@ -223,7 +274,7 @@ function primitiveType2JSName(type) {
switch (type) {
case 'char':
case 'byte':
case 'uin8':
case 'uint8':
case 'int8':
case 'int16':
case 'uint16':
Expand Down Expand Up @@ -256,7 +307,7 @@ function fileName2Typeclass(filename) {
const array = filename.split(regex).filter(Boolean);

if (!array || array.length != 3) {
// todo: throw error
// todo: throw error
console.log('ERRORRROOROR', array);
return;
}
Expand Down
34 changes: 17 additions & 17 deletions types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@

// eslint-disable-next-line spaced-comment
/// <reference path="base.d.ts" />
/// <reference path="base.d.ts" />

declare module 'rclnodejs' {

/**
* Create a node.
*
*
* @remarks
* See {@link Node}
*
*
* @param nodeName - The name used to register in ROS.
* @param namespace - The namespace used in ROS, default is an empty string.
* @param context - The context, default is Context.defaultContext().
Expand All @@ -19,15 +18,15 @@ declare module 'rclnodejs' {

/**
* Init the module.
*
*
* @param context - The context, default is Context.defaultContext().
* @returns A Promise.
*/
function init(context?: Context): Promise<void>;

/**
* Spin up the node event loop to check for incoming events.
*
*
* @param node - The node to be spun.
* @param timeout - ms to wait, block forever if negative, return immediately when 0, default is 10.
*/
Expand All @@ -43,52 +42,53 @@ declare module 'rclnodejs' {

/**
* Stop all activity, destroy all nodes and node components.
*
*
* @param context - The context, default is Context.defaultContext()
*/
function shutdown(context?: Context): void;

/**
* Test if the module is shutdown.
*
*
* @returns True if the module is shut down, otherwise return false.
*/
function isShutdown(): boolean;

/**
* Get the interface package, which is used by publisher/subscription or client/service.
*
*
* @param name - The name of interface to be required.
* @returns The object of the required package/interface.
*/
function require<T extends MessageTypeClassName>(name: T): MessageWrapperType<T>;
function require(name: string): object;

/**
* Generate JavaScript structs files from the IDL of
* messages(.msg) and services(.srv).
* messages(.msg) and services(.srv).
* Search packages which locate under path $AMENT_PREFIX_PATH
* and output JS files into the 'generated' folder.
* and output JS files into the 'generated' folder.
* Any existing files under the generated folder will
* be overwritten.
*
*
* @returns A Promise.
*/
function regenerateAll(): Promise<void>;

/**
* Judge if the topic or service is hidden,
*
* Judge if the topic or service is hidden,
*
* @remarks
* See {@link http://design.ros2.org/articles/topic_and_service_names.html#hidden-topic-or-service-names}
*
*
* @param name - Name of topic or service.
* @returns True if a given topic or service name is hidden, otherwise False.
*/
function isTopicOrServiceHidden(name: string): boolean;

/**
* Expand a given topic name using given node name and namespace.
*
*
* @param topicName - Topic name to be expanded.
* @param nodeName - Name of the node that this topic is associated with.
* @param nodeNamespace - Namespace that the topic is within.
Expand All @@ -99,7 +99,7 @@ declare module 'rclnodejs' {

/**
* Create a plain JavaScript message object.
*
*
* @param type - type identifier, acceptable formats could be 'std_msgs/std/String'
* or {package: 'std_msgs', type: 'msg', name: 'String'}
* @returns A Message object or undefined if type is not recognized.
Expand Down
Loading

0 comments on commit a04c6d2

Please sign in to comment.