Skip to content

Commit 6bf2e77

Browse files
mahmoud-ghalayiniminggangw
authored andcommitted
Add MessageIntrospector for message schema inspection (#1346)
MessageIntrospector, a utility class that provides a simplified API for inspecting ROS 2 message structure without directly using loader.loadInterface. - typeName: Returns the full message type (e.g., geometry_msgs/msg/Twist) - fields:- Returns an array of top-level field names - defaults: Returns a deep copy of the message's default values - schema: Returns the underlying ROSMessageDef object - typeClass: Returns the underlying constructor
1 parent 815b0e5 commit 6bf2e77

10 files changed

Lines changed: 440 additions & 4 deletions

CONTRIBUTORS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
- Add structured error handling with class error hierarchy
4646
- Add ParameterWatcher for real-time parameter monitoring
4747
- Enhance Message Validation
48+
- Add TypeScript definitions and non-throwing variants for validator
49+
- Add MessageIntrospector for message schema inspection
4850

4951
- **[Martins Mozeiko](https://github.com/martins-mozeiko)**
5052
- QoS new/delete fix
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# ROS 2 MessageIntrospector Example
2+
3+
This directory contains an example demonstrating the `MessageIntrospector` class for inspecting ROS 2 message structure.
4+
5+
## Overview
6+
7+
The `MessageIntrospector` class provides a simple way to understand the structure of ROS 2 messages without directly using `loader.loadInterface`. It's useful for debugging, generating documentation, and building dynamic UIs based on message structure.
8+
9+
## MessageIntrospector Example (`message-introspector-example.js`)
10+
11+
**Purpose**: Demonstrates how to inspect message structure, fields, and default values.
12+
13+
### Run Command
14+
15+
```bash
16+
node message-introspector-example.js
17+
```
18+
19+
### Expected Output
20+
21+
```
22+
Twist fields: [ 'linear', 'angular' ]
23+
Twist defaults: { linear: { x: 0, y: 0, z: 0 }, angular: { x: 0, y: 0, z: 0 } }
24+
String fields: [ 'data' ]
25+
String defaults: { data: '' }
26+
JointState fields: [ 'header', 'name', 'position', 'velocity', 'effort' ]
27+
Twist schema msgName: Twist
28+
```
29+
30+
## API
31+
32+
```javascript
33+
const Twist = new rclnodejs.MessageIntrospector('geometry_msgs/msg/Twist');
34+
35+
Twist.typeName; // 'geometry_msgs/msg/Twist'
36+
Twist.fields; // ['linear', 'angular']
37+
Twist.defaults; // { linear: { x: 0, y: 0, z: 0 }, angular: { x: 0, y: 0, z: 0 } }
38+
Twist.schema; // ROSMessageDef object
39+
Twist.typeClass; // The underlying constructor
40+
```
41+
42+
## Notes
43+
44+
- Works with any message type including custom packages
45+
- Default values are cached for performance after the first access
46+
- The `defaults` getter returns a new deep copy each time to prevent mutation
47+
- Service request/response types can also be inspected (e.g., `'my_pkg/srv/MyService_Request'`)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) 2025 Mahmoud Alghalayini. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
const rclnodejs = require('../../index.js');
18+
19+
/**
20+
* This example demonstrates the MessageIntrospector class for
21+
* inspecting ROS 2 message structure without using loader.loadInterface.
22+
*/
23+
async function main() {
24+
await rclnodejs.init();
25+
26+
const Twist = new rclnodejs.MessageIntrospector('geometry_msgs/msg/Twist');
27+
const String = new rclnodejs.MessageIntrospector('std_msgs/msg/String');
28+
const JointState = new rclnodejs.MessageIntrospector(
29+
'sensor_msgs/msg/JointState'
30+
);
31+
32+
console.log('Twist fields:', Twist.fields);
33+
console.log('Twist defaults:', Twist.defaults);
34+
35+
console.log('String fields:', String.fields);
36+
console.log('String defaults:', String.defaults);
37+
38+
console.log('JointState fields:', JointState.fields);
39+
40+
console.log('Twist schema msgName:', Twist.schema.msgName);
41+
42+
await rclnodejs.shutdown();
43+
}
44+
45+
main();

index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const {
6161
const ParameterClient = require('./lib/parameter_client.js');
6262
const errors = require('./lib/errors.js');
6363
const ParameterWatcher = require('./lib/parameter_watcher.js');
64+
const MessageIntrospector = require('./lib/message_introspector.js');
6465
const { spawn } = require('child_process');
6566
const {
6667
ValidationProblem,
@@ -721,3 +722,6 @@ const Lifecycle = require('./lib/lifecycle.js');
721722

722723
/** Lifecycle namespace */
723724
rcl.lifecycle = Lifecycle;
725+
726+
/** {@link MessageIntrospector} class */
727+
rcl.MessageIntrospector = MessageIntrospector;

lib/message_introspector.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright (c) 2025 Mahmoud Alghalayini. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
const loader = require('./interface_loader.js');
18+
const { toPlainObject } = require('../rosidl_gen/message_translator.js');
19+
const { TypeValidationError } = require('./errors.js');
20+
21+
/**
22+
* A utility class for inspecting ROS 2 message structure without using loader.loadInterface directly.
23+
* Provides access to message schema, field names, and default values.
24+
*/
25+
class MessageIntrospector {
26+
#typeClass;
27+
#typeName;
28+
#defaultsCache;
29+
30+
/**
31+
* Create a new MessageIntrospector for a ROS 2 message type.
32+
* @param {string} typeName - The full message type name (e.g., 'geometry_msgs/msg/Twist')
33+
* @throws {TypeValidationError} If typeName is not a non-empty string
34+
* @throws {Error} If the message type cannot be loaded
35+
*/
36+
constructor(typeName) {
37+
if (!typeName || typeof typeName !== 'string') {
38+
throw new TypeValidationError('typeName', typeName, 'non-empty string', {
39+
entityType: 'MessageIntrospector',
40+
});
41+
}
42+
this.#typeName = typeName;
43+
this.#typeClass = loader.loadInterface(typeName);
44+
this.#defaultsCache = null;
45+
}
46+
47+
/**
48+
* Get the full message type name.
49+
* @returns {string} The message type name (e.g., 'geometry_msgs/msg/Twist')
50+
*/
51+
get typeName() {
52+
return this.#typeName;
53+
}
54+
55+
/**
56+
* Get the underlying ROS message class.
57+
* @returns {Function} The message type class constructor
58+
*/
59+
get typeClass() {
60+
return this.#typeClass;
61+
}
62+
63+
/**
64+
* Get the field names of the message.
65+
* @returns {string[]} Array of field names
66+
*/
67+
get fields() {
68+
const def = this.#typeClass.ROSMessageDef;
69+
return def.fields.map((f) => f.name);
70+
}
71+
72+
/**
73+
* Get the ROSMessageDef schema for the message type.
74+
* @returns {object} The message definition schema
75+
*/
76+
get schema() {
77+
return this.#typeClass.ROSMessageDef;
78+
}
79+
80+
/**
81+
* Get the default values for all fields.
82+
* Creates a new instance of the message and converts it to a plain object.
83+
* Result is cached for performance.
84+
* @returns {object} A plain object with all default values
85+
*/
86+
get defaults() {
87+
if (this.#defaultsCache === null) {
88+
const instance = new this.#typeClass();
89+
this.#defaultsCache = toPlainObject(instance);
90+
}
91+
return this.#deepClone(this.#defaultsCache);
92+
}
93+
94+
/**
95+
* Deep clone an object.
96+
* @param {any} obj - Object to clone
97+
* @returns {any} Cloned object
98+
* @private
99+
*/
100+
#deepClone(obj) {
101+
if (obj === null || typeof obj !== 'object') {
102+
return obj;
103+
}
104+
105+
if (Array.isArray(obj)) {
106+
return obj.map((item) => this.#deepClone(item));
107+
}
108+
109+
if (ArrayBuffer.isView(obj) && !(obj instanceof DataView)) {
110+
return obj.slice();
111+
}
112+
113+
const cloned = {};
114+
for (const key in obj) {
115+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
116+
cloned[key] = this.#deepClone(obj[key]);
117+
}
118+
}
119+
return cloned;
120+
}
121+
}
122+
123+
module.exports = MessageIntrospector;

src/rcl_logging_bindings.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Napi::Value InitRosoutPublisherForNode(const Napi::CallbackInfo& info) {
4141
.ThrowAsJavaScriptException();
4242
rcl_reset_error();
4343
}
44-
}
44+
}
4545
return env.Undefined();
4646
}
4747

src/rcl_utilities.cpp

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -369,9 +369,8 @@ void ThrowIfUnparsedROSArgs(Napi::Env env, const Napi::Array& jsArgv,
369369
return;
370370
}
371371

372-
RCPPUTILS_SCOPE_EXIT({
373-
allocator.deallocate(unparsed_indices_c, allocator.state);
374-
});
372+
RCPPUTILS_SCOPE_EXIT(
373+
{ allocator.deallocate(unparsed_indices_c, allocator.state); });
375374

376375
std::string unparsed_args_str = "[";
377376
for (int i = 0; i < unparsed_ros_args_count; ++i) {

0 commit comments

Comments
 (0)